You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

560 lines
16 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
Verifier circuit (#23) * ECC scalar multiplication (first draft) * fix clippy nits * start implementing the ro gadget: 1st design Poseidon + truncate * truncate to 128 bits * implement add + double in constraints * finish implementing constraints for ecc * cargo fmt * input of smul should be an array of bits * cleanup ro a bit. Make the challenge returned be a vec of allocated bits * switch to neptune 6.0 * start implementing high level circuit * incomplete version of the verifier circuit with many TODOS * optimize ecc ops. add i ==0 case to the circuit * fix 0/1 constants at the circuit * wrap CompressedGroupElement of Pallas and Vesta * cargo fmt * generate poseidon constants once instead of every time we call get_challenge * Implement RO-based poseidon to use outside of circuit. Reorganize the repo * add inner circuit to verification circuit * start adding folding of the io. there is an error in the first call to mult_mod * add test to check that bellperson-nonnative is compatible with nova * remove swap file * add another test that fails * add inputs to the circuits in tests * rename q to m in circuit.rs. add more tests in test_bellperson_non_native. change a in test_mult_mod to expose error * push test for equal_with_carried. fix the issue is src/r1cs.rs * cargo fmt + update the verifier circuit: add folding of X and update all hashes with X * make limb_width and n_limbs parameters * make params part of h1 * allocate the field order as constant. add check that z0 == zi when i == 0 * fix error in test_poseidon_ro * remove merge error * small fixes * small fixes to comments * clippy lints * small edits; rename tests * move inputize before from_num * _limbs --> _bn * _limbs --> _bn Co-authored-by: Ioanna <iontzialla@gmail.com>
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. //! This module defines R1CS related types and a folding scheme for Relaxed R1CS
  2. #![allow(clippy::type_complexity)]
  3. use crate::{
  4. constants::{BN_LIMB_WIDTH, BN_N_LIMBS},
  5. errors::NovaError,
  6. gadgets::{
  7. nonnative::{bignat::nat_to_limbs, util::f_to_nat},
  8. utils::scalar_as_base,
  9. },
  10. traits::{
  11. commitment::CommitmentEngineTrait, AbsorbInROTrait, Group, ROTrait, TranscriptReprTrait,
  12. },
  13. Commitment, CommitmentKey, CE,
  14. };
  15. use core::{cmp::max, marker::PhantomData};
  16. use ff::Field;
  17. use itertools::concat;
  18. use rayon::prelude::*;
  19. use serde::{Deserialize, Serialize};
  20. /// Public parameters for a given R1CS
  21. #[derive(Clone, Serialize, Deserialize)]
  22. #[serde(bound = "")]
  23. pub struct R1CS<G: Group> {
  24. _p: PhantomData<G>,
  25. }
  26. /// A type that holds the shape of the R1CS matrices
  27. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
  28. pub struct R1CSShape<G: Group> {
  29. pub(crate) num_cons: usize,
  30. pub(crate) num_vars: usize,
  31. pub(crate) num_io: usize,
  32. pub(crate) A: Vec<(usize, usize, G::Scalar)>,
  33. pub(crate) B: Vec<(usize, usize, G::Scalar)>,
  34. pub(crate) C: Vec<(usize, usize, G::Scalar)>,
  35. }
  36. /// A type that holds a witness for a given R1CS instance
  37. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
  38. pub struct R1CSWitness<G: Group> {
  39. W: Vec<G::Scalar>,
  40. }
  41. /// A type that holds an R1CS instance
  42. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
  43. #[serde(bound = "")]
  44. pub struct R1CSInstance<G: Group> {
  45. pub(crate) comm_W: Commitment<G>,
  46. pub(crate) X: Vec<G::Scalar>,
  47. }
  48. /// A type that holds a witness for a given Relaxed R1CS instance
  49. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
  50. pub struct RelaxedR1CSWitness<G: Group> {
  51. pub(crate) W: Vec<G::Scalar>,
  52. pub(crate) E: Vec<G::Scalar>,
  53. }
  54. /// A type that holds a Relaxed R1CS instance
  55. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
  56. #[serde(bound = "")]
  57. pub struct RelaxedR1CSInstance<G: Group> {
  58. pub(crate) comm_W: Commitment<G>,
  59. pub(crate) comm_E: Commitment<G>,
  60. pub(crate) X: Vec<G::Scalar>,
  61. pub(crate) u: G::Scalar,
  62. }
  63. impl<G: Group> R1CS<G> {
  64. /// Samples public parameters for the specified number of constraints and variables in an R1CS
  65. pub fn commitment_key(S: &R1CSShape<G>) -> CommitmentKey<G> {
  66. let num_cons = S.num_cons;
  67. let num_vars = S.num_vars;
  68. let total_nz = S.A.len() + S.B.len() + S.C.len();
  69. G::CE::setup(b"ck", max(max(num_cons, num_vars), total_nz))
  70. }
  71. }
  72. impl<G: Group> R1CSShape<G> {
  73. /// Create an object of type `R1CSShape` from the explicitly specified R1CS matrices
  74. pub fn new(
  75. num_cons: usize,
  76. num_vars: usize,
  77. num_io: usize,
  78. A: &[(usize, usize, G::Scalar)],
  79. B: &[(usize, usize, G::Scalar)],
  80. C: &[(usize, usize, G::Scalar)],
  81. ) -> Result<R1CSShape<G>, NovaError> {
  82. let is_valid = |num_cons: usize,
  83. num_vars: usize,
  84. num_io: usize,
  85. M: &[(usize, usize, G::Scalar)]|
  86. -> Result<(), NovaError> {
  87. let res = (0..M.len())
  88. .map(|i| {
  89. let (row, col, _val) = M[i];
  90. if row >= num_cons || col > num_io + num_vars {
  91. Err(NovaError::InvalidIndex)
  92. } else {
  93. Ok(())
  94. }
  95. })
  96. .collect::<Result<Vec<()>, NovaError>>();
  97. if res.is_err() {
  98. Err(NovaError::InvalidIndex)
  99. } else {
  100. Ok(())
  101. }
  102. };
  103. let res_A = is_valid(num_cons, num_vars, num_io, A);
  104. let res_B = is_valid(num_cons, num_vars, num_io, B);
  105. let res_C = is_valid(num_cons, num_vars, num_io, C);
  106. if res_A.is_err() || res_B.is_err() || res_C.is_err() {
  107. return Err(NovaError::InvalidIndex);
  108. }
  109. // We require the number of public inputs/outputs to be even
  110. if num_io % 2 != 0 {
  111. return Err(NovaError::OddInputLength);
  112. }
  113. Ok(R1CSShape {
  114. num_cons,
  115. num_vars,
  116. num_io,
  117. A: A.to_owned(),
  118. B: B.to_owned(),
  119. C: C.to_owned(),
  120. })
  121. }
  122. pub fn multiply_vec(
  123. &self,
  124. z: &[G::Scalar],
  125. ) -> Result<(Vec<G::Scalar>, Vec<G::Scalar>, Vec<G::Scalar>), NovaError> {
  126. if z.len() != self.num_io + self.num_vars + 1 {
  127. return Err(NovaError::InvalidWitnessLength);
  128. }
  129. // computes a product between a sparse matrix `M` and a vector `z`
  130. // This does not perform any validation of entries in M (e.g., if entries in `M` reference indexes outside the range of `z`)
  131. // This is safe since we know that `M` is valid
  132. let sparse_matrix_vec_product =
  133. |M: &Vec<(usize, usize, G::Scalar)>, num_rows: usize, z: &[G::Scalar]| -> Vec<G::Scalar> {
  134. (0..M.len())
  135. .map(|i| {
  136. let (row, col, val) = M[i];
  137. (row, val * z[col])
  138. })
  139. .fold(vec![G::Scalar::ZERO; num_rows], |mut Mz, (r, v)| {
  140. Mz[r] += v;
  141. Mz
  142. })
  143. };
  144. let (Az, (Bz, Cz)) = rayon::join(
  145. || sparse_matrix_vec_product(&self.A, self.num_cons, z),
  146. || {
  147. rayon::join(
  148. || sparse_matrix_vec_product(&self.B, self.num_cons, z),
  149. || sparse_matrix_vec_product(&self.C, self.num_cons, z),
  150. )
  151. },
  152. );
  153. Ok((Az, Bz, Cz))
  154. }
  155. /// Checks if the Relaxed R1CS instance is satisfiable given a witness and its shape
  156. pub fn is_sat_relaxed(
  157. &self,
  158. ck: &CommitmentKey<G>,
  159. U: &RelaxedR1CSInstance<G>,
  160. W: &RelaxedR1CSWitness<G>,
  161. ) -> Result<(), NovaError> {
  162. assert_eq!(W.W.len(), self.num_vars);
  163. assert_eq!(W.E.len(), self.num_cons);
  164. assert_eq!(U.X.len(), self.num_io);
  165. // verify if Az * Bz = u*Cz + E
  166. let res_eq: bool = {
  167. let z = concat(vec![W.W.clone(), vec![U.u], U.X.clone()]);
  168. let (Az, Bz, Cz) = self.multiply_vec(&z)?;
  169. assert_eq!(Az.len(), self.num_cons);
  170. assert_eq!(Bz.len(), self.num_cons);
  171. assert_eq!(Cz.len(), self.num_cons);
  172. let res: usize = (0..self.num_cons)
  173. .map(|i| usize::from(Az[i] * Bz[i] != U.u * Cz[i] + W.E[i]))
  174. .sum();
  175. res == 0
  176. };
  177. // verify if comm_E and comm_W are commitments to E and W
  178. let res_comm: bool = {
  179. let (comm_W, comm_E) =
  180. rayon::join(|| CE::<G>::commit(ck, &W.W), || CE::<G>::commit(ck, &W.E));
  181. U.comm_W == comm_W && U.comm_E == comm_E
  182. };
  183. if res_eq && res_comm {
  184. Ok(())
  185. } else {
  186. Err(NovaError::UnSat)
  187. }
  188. }
  189. /// Checks if the R1CS instance is satisfiable given a witness and its shape
  190. pub fn is_sat(
  191. &self,
  192. ck: &CommitmentKey<G>,
  193. U: &R1CSInstance<G>,
  194. W: &R1CSWitness<G>,
  195. ) -> Result<(), NovaError> {
  196. assert_eq!(W.W.len(), self.num_vars);
  197. assert_eq!(U.X.len(), self.num_io);
  198. // verify if Az * Bz = u*Cz
  199. let res_eq: bool = {
  200. let z = concat(vec![W.W.clone(), vec![G::Scalar::ONE], U.X.clone()]);
  201. let (Az, Bz, Cz) = self.multiply_vec(&z)?;
  202. assert_eq!(Az.len(), self.num_cons);
  203. assert_eq!(Bz.len(), self.num_cons);
  204. assert_eq!(Cz.len(), self.num_cons);
  205. let res: usize = (0..self.num_cons)
  206. .map(|i| usize::from(Az[i] * Bz[i] != Cz[i]))
  207. .sum();
  208. res == 0
  209. };
  210. // verify if comm_W is a commitment to W
  211. let res_comm: bool = U.comm_W == CE::<G>::commit(ck, &W.W);
  212. if res_eq && res_comm {
  213. Ok(())
  214. } else {
  215. Err(NovaError::UnSat)
  216. }
  217. }
  218. /// A method to compute a commitment to the cross-term `T` given a
  219. /// Relaxed R1CS instance-witness pair and an R1CS instance-witness pair
  220. pub fn commit_T(
  221. &self,
  222. ck: &CommitmentKey<G>,
  223. U1: &RelaxedR1CSInstance<G>,
  224. W1: &RelaxedR1CSWitness<G>,
  225. U2: &R1CSInstance<G>,
  226. W2: &R1CSWitness<G>,
  227. ) -> Result<(Vec<G::Scalar>, Commitment<G>), NovaError> {
  228. let (AZ_1, BZ_1, CZ_1) = {
  229. let Z1 = concat(vec![W1.W.clone(), vec![U1.u], U1.X.clone()]);
  230. self.multiply_vec(&Z1)?
  231. };
  232. let (AZ_2, BZ_2, CZ_2) = {
  233. let Z2 = concat(vec![W2.W.clone(), vec![G::Scalar::ONE], U2.X.clone()]);
  234. self.multiply_vec(&Z2)?
  235. };
  236. let AZ_1_circ_BZ_2 = (0..AZ_1.len())
  237. .into_par_iter()
  238. .map(|i| AZ_1[i] * BZ_2[i])
  239. .collect::<Vec<G::Scalar>>();
  240. let AZ_2_circ_BZ_1 = (0..AZ_2.len())
  241. .into_par_iter()
  242. .map(|i| AZ_2[i] * BZ_1[i])
  243. .collect::<Vec<G::Scalar>>();
  244. let u_1_cdot_CZ_2 = (0..CZ_2.len())
  245. .into_par_iter()
  246. .map(|i| U1.u * CZ_2[i])
  247. .collect::<Vec<G::Scalar>>();
  248. let u_2_cdot_CZ_1 = (0..CZ_1.len())
  249. .into_par_iter()
  250. .map(|i| CZ_1[i])
  251. .collect::<Vec<G::Scalar>>();
  252. let T = AZ_1_circ_BZ_2
  253. .par_iter()
  254. .zip(&AZ_2_circ_BZ_1)
  255. .zip(&u_1_cdot_CZ_2)
  256. .zip(&u_2_cdot_CZ_1)
  257. .map(|(((a, b), c), d)| *a + *b - *c - *d)
  258. .collect::<Vec<G::Scalar>>();
  259. let comm_T = CE::<G>::commit(ck, &T);
  260. Ok((T, comm_T))
  261. }
  262. /// Pads the R1CSShape so that the number of variables is a power of two
  263. /// Renumbers variables to accomodate padded variables
  264. pub fn pad(&self) -> Self {
  265. // equalize the number of variables and constraints
  266. let m = max(self.num_vars, self.num_cons).next_power_of_two();
  267. // check if the provided R1CSShape is already as required
  268. if self.num_vars == m && self.num_cons == m {
  269. return self.clone();
  270. }
  271. // check if the number of variables are as expected, then
  272. // we simply set the number of constraints to the next power of two
  273. if self.num_vars == m {
  274. return R1CSShape {
  275. num_cons: m,
  276. num_vars: m,
  277. num_io: self.num_io,
  278. A: self.A.clone(),
  279. B: self.B.clone(),
  280. C: self.C.clone(),
  281. };
  282. }
  283. // otherwise, we need to pad the number of variables and renumber variable accesses
  284. let num_vars_padded = m;
  285. let num_cons_padded = m;
  286. let apply_pad = |M: &[(usize, usize, G::Scalar)]| -> Vec<(usize, usize, G::Scalar)> {
  287. M.par_iter()
  288. .map(|(r, c, v)| {
  289. (
  290. *r,
  291. if c >= &self.num_vars {
  292. c + num_vars_padded - self.num_vars
  293. } else {
  294. *c
  295. },
  296. *v,
  297. )
  298. })
  299. .collect::<Vec<_>>()
  300. };
  301. let A_padded = apply_pad(&self.A);
  302. let B_padded = apply_pad(&self.B);
  303. let C_padded = apply_pad(&self.C);
  304. R1CSShape {
  305. num_cons: num_cons_padded,
  306. num_vars: num_vars_padded,
  307. num_io: self.num_io,
  308. A: A_padded,
  309. B: B_padded,
  310. C: C_padded,
  311. }
  312. }
  313. }
  314. impl<G: Group> R1CSWitness<G> {
  315. /// A method to create a witness object using a vector of scalars
  316. pub fn new(S: &R1CSShape<G>, W: &[G::Scalar]) -> Result<R1CSWitness<G>, NovaError> {
  317. if S.num_vars != W.len() {
  318. Err(NovaError::InvalidWitnessLength)
  319. } else {
  320. Ok(R1CSWitness { W: W.to_owned() })
  321. }
  322. }
  323. /// Commits to the witness using the supplied generators
  324. pub fn commit(&self, ck: &CommitmentKey<G>) -> Commitment<G> {
  325. CE::<G>::commit(ck, &self.W)
  326. }
  327. }
  328. impl<G: Group> R1CSInstance<G> {
  329. /// A method to create an instance object using consitituent elements
  330. pub fn new(
  331. S: &R1CSShape<G>,
  332. comm_W: &Commitment<G>,
  333. X: &[G::Scalar],
  334. ) -> Result<R1CSInstance<G>, NovaError> {
  335. if S.num_io != X.len() {
  336. Err(NovaError::InvalidInputLength)
  337. } else {
  338. Ok(R1CSInstance {
  339. comm_W: *comm_W,
  340. X: X.to_owned(),
  341. })
  342. }
  343. }
  344. }
  345. impl<G: Group> AbsorbInROTrait<G> for R1CSInstance<G> {
  346. fn absorb_in_ro(&self, ro: &mut G::RO) {
  347. self.comm_W.absorb_in_ro(ro);
  348. for x in &self.X {
  349. ro.absorb(scalar_as_base::<G>(*x));
  350. }
  351. }
  352. }
  353. impl<G: Group> RelaxedR1CSWitness<G> {
  354. /// Produces a default RelaxedR1CSWitness given an R1CSShape
  355. pub fn default(S: &R1CSShape<G>) -> RelaxedR1CSWitness<G> {
  356. RelaxedR1CSWitness {
  357. W: vec![G::Scalar::ZERO; S.num_vars],
  358. E: vec![G::Scalar::ZERO; S.num_cons],
  359. }
  360. }
  361. /// Initializes a new RelaxedR1CSWitness from an R1CSWitness
  362. pub fn from_r1cs_witness(S: &R1CSShape<G>, witness: &R1CSWitness<G>) -> RelaxedR1CSWitness<G> {
  363. RelaxedR1CSWitness {
  364. W: witness.W.clone(),
  365. E: vec![G::Scalar::ZERO; S.num_cons],
  366. }
  367. }
  368. /// Commits to the witness using the supplied generators
  369. pub fn commit(&self, ck: &CommitmentKey<G>) -> (Commitment<G>, Commitment<G>) {
  370. (CE::<G>::commit(ck, &self.W), CE::<G>::commit(ck, &self.E))
  371. }
  372. /// Folds an incoming R1CSWitness into the current one
  373. pub fn fold(
  374. &self,
  375. W2: &R1CSWitness<G>,
  376. T: &[G::Scalar],
  377. r: &G::Scalar,
  378. ) -> Result<RelaxedR1CSWitness<G>, NovaError> {
  379. let (W1, E1) = (&self.W, &self.E);
  380. let W2 = &W2.W;
  381. if W1.len() != W2.len() {
  382. return Err(NovaError::InvalidWitnessLength);
  383. }
  384. let W = W1
  385. .par_iter()
  386. .zip(W2)
  387. .map(|(a, b)| *a + *r * *b)
  388. .collect::<Vec<G::Scalar>>();
  389. let E = E1
  390. .par_iter()
  391. .zip(T)
  392. .map(|(a, b)| *a + *r * *b)
  393. .collect::<Vec<G::Scalar>>();
  394. Ok(RelaxedR1CSWitness { W, E })
  395. }
  396. /// Pads the provided witness to the correct length
  397. pub fn pad(&self, S: &R1CSShape<G>) -> RelaxedR1CSWitness<G> {
  398. let W = {
  399. let mut W = self.W.clone();
  400. W.extend(vec![G::Scalar::ZERO; S.num_vars - W.len()]);
  401. W
  402. };
  403. let E = {
  404. let mut E = self.E.clone();
  405. E.extend(vec![G::Scalar::ZERO; S.num_cons - E.len()]);
  406. E
  407. };
  408. Self { W, E }
  409. }
  410. }
  411. impl<G: Group> RelaxedR1CSInstance<G> {
  412. /// Produces a default RelaxedR1CSInstance given R1CSGens and R1CSShape
  413. pub fn default(_ck: &CommitmentKey<G>, S: &R1CSShape<G>) -> RelaxedR1CSInstance<G> {
  414. let (comm_W, comm_E) = (Commitment::<G>::default(), Commitment::<G>::default());
  415. RelaxedR1CSInstance {
  416. comm_W,
  417. comm_E,
  418. u: G::Scalar::ZERO,
  419. X: vec![G::Scalar::ZERO; S.num_io],
  420. }
  421. }
  422. /// Initializes a new RelaxedR1CSInstance from an R1CSInstance
  423. pub fn from_r1cs_instance(
  424. ck: &CommitmentKey<G>,
  425. S: &R1CSShape<G>,
  426. instance: &R1CSInstance<G>,
  427. ) -> RelaxedR1CSInstance<G> {
  428. let mut r_instance = RelaxedR1CSInstance::default(ck, S);
  429. r_instance.comm_W = instance.comm_W;
  430. r_instance.u = G::Scalar::ONE;
  431. r_instance.X = instance.X.clone();
  432. r_instance
  433. }
  434. /// Initializes a new RelaxedR1CSInstance from an R1CSInstance
  435. pub fn from_r1cs_instance_unchecked(
  436. comm_W: &Commitment<G>,
  437. X: &[G::Scalar],
  438. ) -> RelaxedR1CSInstance<G> {
  439. RelaxedR1CSInstance {
  440. comm_W: *comm_W,
  441. comm_E: Commitment::<G>::default(),
  442. u: G::Scalar::ONE,
  443. X: X.to_vec(),
  444. }
  445. }
  446. /// Folds an incoming RelaxedR1CSInstance into the current one
  447. pub fn fold(
  448. &self,
  449. U2: &R1CSInstance<G>,
  450. comm_T: &Commitment<G>,
  451. r: &G::Scalar,
  452. ) -> Result<RelaxedR1CSInstance<G>, NovaError> {
  453. let (X1, u1, comm_W_1, comm_E_1) =
  454. (&self.X, &self.u, &self.comm_W.clone(), &self.comm_E.clone());
  455. let (X2, comm_W_2) = (&U2.X, &U2.comm_W);
  456. // weighted sum of X, comm_W, comm_E, and u
  457. let X = X1
  458. .par_iter()
  459. .zip(X2)
  460. .map(|(a, b)| *a + *r * *b)
  461. .collect::<Vec<G::Scalar>>();
  462. let comm_W = *comm_W_1 + *comm_W_2 * *r;
  463. let comm_E = *comm_E_1 + *comm_T * *r;
  464. let u = *u1 + *r;
  465. Ok(RelaxedR1CSInstance {
  466. comm_W,
  467. comm_E,
  468. X,
  469. u,
  470. })
  471. }
  472. }
  473. impl<G: Group> TranscriptReprTrait<G> for RelaxedR1CSInstance<G> {
  474. fn to_transcript_bytes(&self) -> Vec<u8> {
  475. [
  476. self.comm_W.to_transcript_bytes(),
  477. self.comm_E.to_transcript_bytes(),
  478. self.u.to_transcript_bytes(),
  479. self.X.as_slice().to_transcript_bytes(),
  480. ]
  481. .concat()
  482. }
  483. }
  484. impl<G: Group> AbsorbInROTrait<G> for RelaxedR1CSInstance<G> {
  485. fn absorb_in_ro(&self, ro: &mut G::RO) {
  486. self.comm_W.absorb_in_ro(ro);
  487. self.comm_E.absorb_in_ro(ro);
  488. ro.absorb(scalar_as_base::<G>(self.u));
  489. // absorb each element of self.X in bignum format
  490. for x in &self.X {
  491. let limbs: Vec<G::Scalar> = nat_to_limbs(&f_to_nat(x), BN_LIMB_WIDTH, BN_N_LIMBS).unwrap();
  492. for limb in limbs {
  493. ro.absorb(scalar_as_base::<G>(limb));
  494. }
  495. }
  496. }
  497. }