ark-r1cs-std/r1cs-std/src/groups/curves/short_weierstrass/mod.rs
r1cs-std/src/groups/curves/short_weierstrass/mod.rsRaw
1use algebra::{AffineCurve, Field, ProjectiveCurve};
2use algebra::{
3 curves::{
4 short_weierstrass_jacobian::{GroupAffine as SWAffine, GroupProjective as SWProjective},
5 SWModelParameters,
6 },
7 BitIterator, PrimeField,
8};
9use r1cs_core::{ConstraintSystem, SynthesisError};
10use std::{borrow::Borrow, marker::PhantomData, ops::Neg};
11
12use crate::Assignment;
13use crate::prelude::*;
14
15pub mod bls12;
16
17#[derive(Derivative)]
18#[derivative(Debug, Clone)]
19#[must_use]
20pub struct AffineGadget<P: SWModelParameters, ConstraintF: Field, F: FieldGadget<P::BaseField, ConstraintF>> {
21 pub x: F,
22 pub y: F,
23 _params: PhantomData<P>,
24 _engine: PhantomData<ConstraintF>,
25}
26
27impl<P: SWModelParameters, ConstraintF: Field, F: FieldGadget<P::BaseField, ConstraintF>>
28 AffineGadget<P, ConstraintF, F>
29{
30 pub fn new(x: F, y: F) -> Self {
31 Self {
32 x,
33 y,
34 _params: PhantomData,
35 _engine: PhantomData,
36 }
37 }
38
39 pub fn alloc_without_check<FN, CS: ConstraintSystem<ConstraintF>>(
40 mut cs: CS,
41 value_gen: F,
42 ) -> Result<Self, SynthesisError>
43 where
44 F: FnOnce() -> Result<SWProjective<P>, SynthesisError>,
45 {
46 let (x, y) = match value_gen() {
47 Ok(fe) => {
48 let fe = fe.into_affine();
49 (Ok(fe.x), Ok(fe.y))
50 },
51 _ => (
52 Err(SynthesisError::AssignmentMissing),
53 Err(SynthesisError::AssignmentMissing),
54 ),
55 };
56
57 let x = F::alloc(&mut cs.ns(|| "x"), || x)?;
58 let y = F::alloc(&mut cs.ns(|| "y"), || y)?;
59
60 Ok(Self::new(x, y))
61 }
62}
63
64impl<P, ConstraintF, F> PartialEq for AffineGadget<P, ConstraintF, F>
65where
66 P: SWModelParameters,
67 ConstraintF: Field,
68 F: FieldGadget<P::BaseField, ConstraintF>,
69{
70 fn eq(&self, other: &Self) -> bool {
71 self.x == other.x && self.y == other.y
72 }
73}
74
75impl<P, ConstraintF, F> Eq for AffineGadget<P, ConstraintF, F>
76where
77 P: SWModelParameters,
78 ConstraintF: Field,
79 F: FieldGadget<P::BaseField, ConstraintF>,
80{
81}
82
83impl<P, ConstraintF, F> GroupGadget<SWProjective<P>, ConstraintF> for AffineGadget<P, ConstraintF, F>
84where
85 P: SWModelParameters,
86 ConstraintF: Field,
87 F: FieldGadget<P::BaseField, ConstraintF>,
88{
89 type Value = SWProjective<P>;
90 type Variable = (F::Variable, F::Variable);
91
92 #[inline]
93 fn get_value(&self) -> Option<Self::Value> {
94 match (self.x.get_value(), self.y.get_value()) {
95 (Some(x), Some(y)) => {
96 let is_zero = x.is_zero() && y.is_one();
97 Some(SWAffine::new(x, y, is_zero).into_projective())
98 },
99 (None, None) => None,
100 _ => unreachable!(),
101 }
102 }
103
104 #[inline]
105 fn get_variable(&self) -> Self::Variable {
106 (self.x.get_variable(), self.y.get_variable())
107 }
108
109 #[inline]
110 fn zero<CS: ConstraintSystem<ConstraintF>>(mut cs: CS) -> Result<Self, SynthesisError> {
111 Ok(Self::new(
112 F::zero(cs.ns(|| "zero"))?,
113 F::one(cs.ns(|| "one"))?,
114 ))
115 }
116
117 #[inline]
118 /// Incomplete addition: neither `self` nor `other` can be the neutral
119 /// element.
120 fn add<CS: ConstraintSystem<ConstraintF>>(
121 &self,
122 mut cs: CS,
123 other: &Self,
124 ) -> Result<Self, SynthesisError> {
125 // lambda = (B.y - A.y)/(B.x - A.x)
126 // C.x = lambda^2 - A.x - B.x
127 // C.y = lambda(A.x - C.x) - A.y
128 //
129 // Special cases:
130 //
131 // doubling: if B.y = A.y and B.x = A.x then lambda is unbound and
132 // C = (lambda^2, lambda^3)
133 //
134 // addition of negative point: if B.y = -A.y and B.x = A.x then no
135 // lambda can satisfy the first equation unless B.y - A.y = 0. But
136 // then this reduces to doubling.
137 //
138 // So we need to check that A.x - B.x != 0, which can be done by
139 // enforcing I * (B.x - A.x) = 1
140
141 let x2_minus_x1 = other.x.sub(cs.ns(|| "x2 - x1"), &self.x)?;
142 let y2_minus_y1 = other.y.sub(cs.ns(|| "y2 - y1"), &self.y)?;
143
144 let inv = x2_minus_x1.inverse(cs.ns(|| "compute inv"))?;
145
146 let lambda = F::alloc(cs.ns(|| "lambda"), || {
147 Ok(y2_minus_y1.get_value().get()? * &inv.get_value().get()?)
148 })?;
149
150 let x_3 = F::alloc(&mut cs.ns(|| "x_3"), || {
151 let lambda_val = lambda.get_value().get()?;
152 let x1 = self.x.get_value().get()?;
153 let x2 = other.x.get_value().get()?;
154 Ok((lambda_val.square() - &x1) - &x2)
155 })?;
156
157 let y_3 = F::alloc(&mut cs.ns(|| "y_3"), || {
158 let lambda_val = lambda.get_value().get()?;
159 let x_1 = self.x.get_value().get()?;
160 let y_1 = self.y.get_value().get()?;
161 let x_3 = x_3.get_value().get()?;
162 Ok(lambda_val * &(x_1 - &x_3) - &y_1)
163 })?;
164
165 // Check lambda
166 lambda.mul_equals(cs.ns(|| "check lambda"), &x2_minus_x1, &y2_minus_y1)?;
167
168 // Check x3
169 let x3_plus_x1_plus_x2 = x_3
170 .add(cs.ns(|| "x3 + x1"), &self.x)?
171 .add(cs.ns(|| "x3 + x1 + x2"), &other.x)?;
172 lambda.mul_equals(cs.ns(|| "check x3"), &lambda, &x3_plus_x1_plus_x2)?;
173
174 // Check y3
175 let y3_plus_y1 = y_3.add(cs.ns(|| "y3 + y1"), &self.y)?;
176 let x1_minus_x3 = self.x.sub(cs.ns(|| "x1 - x3"), &x_3)?;
177
178 lambda.mul_equals(cs.ns(|| ""), &x1_minus_x3, &y3_plus_y1)?;
179
180 Ok(Self::new(x_3, y_3))
181 }
182
183 /// Incomplete addition: neither `self` nor `other` can be the neutral
184 /// element.
185 fn add_constant<CS: ConstraintSystem<ConstraintF>>(
186 &self,
187 mut cs: CS,
188 other: &SWProjective<P>,
189 ) -> Result<Self, SynthesisError> {
190 // lambda = (B.y - A.y)/(B.x - A.x)
191 // C.x = lambda^2 - A.x - B.x
192 // C.y = lambda(A.x - C.x) - A.y
193 //
194 // Special cases:
195 //
196 // doubling: if B.y = A.y and B.x = A.x then lambda is unbound and
197 // C = (lambda^2, lambda^3)
198 //
199 // addition of negative point: if B.y = -A.y and B.x = A.x then no
200 // lambda can satisfy the first equation unless B.y - A.y = 0. But
201 // then this reduces to doubling.
202 //
203 // So we need to check that A.x - B.x != 0, which can be done by
204 // enforcing I * (B.x - A.x) = 1
205 if other.is_zero() {
206 return Err(SynthesisError::AssignmentMissing);
207 }
208 let other = other.into_affine();
209 let other_x = other.x;
210 let other_y = other.y;
211
212 let x2_minus_x1 = self
213 .x
214 .sub_constant(cs.ns(|| "x2 - x1"), &other_x)?
215 .negate(cs.ns(|| "neg1"))?;
216 let y2_minus_y1 = self
217 .y
218 .sub_constant(cs.ns(|| "y2 - y1"), &other_y)?
219 .negate(cs.ns(|| "neg2"))?;
220
221 let inv = x2_minus_x1.inverse(cs.ns(|| "compute inv"))?;
222
223 let lambda = F::alloc(cs.ns(|| "lambda"), || {
224 Ok(y2_minus_y1.get_value().get()? * &inv.get_value().get()?)
225 })?;
226
227 let x_3 = F::alloc(&mut cs.ns(|| "x_3"), || {
228 let lambda_val = lambda.get_value().get()?;
229 let x1 = self.x.get_value().get()?;
230 let x2 = other_x;
231 Ok((lambda_val.square() - &x1) - &x2)
232 })?;
233
234 let y_3 = F::alloc(&mut cs.ns(|| "y_3"), || {
235 let lambda_val = lambda.get_value().get()?;
236 let x_1 = self.x.get_value().get()?;
237 let y_1 = self.y.get_value().get()?;
238 let x_3 = x_3.get_value().get()?;
239 Ok(lambda_val * &(x_1 - &x_3) - &y_1)
240 })?;
241
242 // Check lambda
243 lambda.mul_equals(cs.ns(|| "check lambda"), &x2_minus_x1, &y2_minus_y1)?;
244
245 // Check x3
246 let x3_plus_x1_plus_x2 = x_3
247 .add(cs.ns(|| "x3 + x1"), &self.x)?
248 .add_constant(cs.ns(|| "x3 + x1 + x2"), &other_x)?;
249 lambda.mul_equals(cs.ns(|| "check x3"), &lambda, &x3_plus_x1_plus_x2)?;
250
251 // Check y3
252 let y3_plus_y1 = y_3.add(cs.ns(|| "y3 + y1"), &self.y)?;
253 let x1_minus_x3 = self.x.sub(cs.ns(|| "x1 - x3"), &x_3)?;
254
255 lambda.mul_equals(cs.ns(|| ""), &x1_minus_x3, &y3_plus_y1)?;
256
257 Ok(Self::new(x_3, y_3))
258 }
259
260 #[inline]
261 fn double_in_place<CS: ConstraintSystem<ConstraintF>>(
262 &mut self,
263 mut cs: CS,
264 ) -> Result<(), SynthesisError> {
265 let a = P::COEFF_A;
266 let x_squared = self.x.square(cs.ns(|| "x^2"))?;
267
268 let one = P::BaseField::one();
269 let two = one.double();
270 let three = two + &one;
271
272 let three_x_squared = x_squared.mul_by_constant(cs.ns(|| "3 * x^2"), &three)?;
273 let three_x_squared_plus_a = three_x_squared.add_constant(cs.ns(|| "3 * x^2 + a"), &a)?;
274
275 let two_y = self.y.double(cs.ns(|| "2y"))?;
276
277 let lambda = F::alloc(cs.ns(|| "lambda"), || {
278 let y_doubled_inv = two_y.get_value().get()?.inverse().get()?;
279 Ok(three_x_squared_plus_a.get_value().get()? * &y_doubled_inv)
280 })?;
281
282 // Check lambda
283 lambda.mul_equals(cs.ns(|| "check lambda"), &two_y, &three_x_squared_plus_a)?;
284
285 let x = lambda
286 .square(cs.ns(|| "lambda^2"))?
287 .sub(cs.ns(|| "lambda^2 - x"), &self.x)?
288 .sub(cs.ns(|| "lambda^2 - 2x"), &self.x)?;
289
290 let y = self
291 .x
292 .sub(cs.ns(|| "x - self.x"), &x)?
293 .mul(cs.ns(|| "times lambda"), &lambda)?
294 .sub(cs.ns(|| "plus self.y"), &self.y)?;
295
296 *self = Self::new(x, y);
297 Ok(())
298 }
299
300 fn negate<CS: ConstraintSystem<ConstraintF>>(&self, mut cs: CS) -> Result<Self, SynthesisError> {
301 Ok(Self::new(
302 self.x.clone(),
303 self.y.negate(cs.ns(|| "negate y"))?,
304 ))
305 }
306
307 fn cost_of_add() -> usize {
308 3 * F::cost_of_mul() + F::cost_of_inv()
309 }
310
311 fn cost_of_double() -> usize {
312 4 * F::cost_of_mul()
313 }
314}
315
316impl<P, ConstraintF, F> CondSelectGadget<ConstraintF> for AffineGadget<P, ConstraintF, F>
317where
318 P: SWModelParameters,
319 ConstraintF: Field,
320 F: FieldGadget<P::BaseField, ConstraintF>,
321{
322 #[inline]
323 fn conditionally_select<CS: ConstraintSystem<ConstraintF>>(
324 mut cs: CS,
325 cond: &Boolean,
326 first: &Self,
327 second: &Self,
328 ) -> Result<Self, SynthesisError> {
329 let x = F::conditionally_select(&mut cs.ns(|| "x"), cond, &first.x, &second.x)?;
330 let y = F::conditionally_select(&mut cs.ns(|| "y"), cond, &first.y, &second.y)?;
331
332 Ok(Self::new(x, y))
333 }
334
335 fn cost() -> usize {
336 2 * <F as CondSelectGadget<ConstraintF>>::cost()
337 }
338}
339
340impl<P, ConstraintF, F> EqGadget<ConstraintF> for AffineGadget<P, ConstraintF, F>
341where
342 P: SWModelParameters,
343 ConstraintF: Field,
344 F: FieldGadget<P::BaseField, ConstraintF>,
345{
346}
347
348impl<P, ConstraintF, F> ConditionalEqGadget<ConstraintF> for AffineGadget<P, ConstraintF, F>
349where
350 P: SWModelParameters,
351 ConstraintF: Field,
352 F: FieldGadget<P::BaseField, ConstraintF>,
353{
354 #[inline]
355 fn conditional_enforce_equal<CS: ConstraintSystem<ConstraintF>>(
356 &self,
357 mut cs: CS,
358 other: &Self,
359 condition: &Boolean,
360 ) -> Result<(), SynthesisError> {
361 self.x.conditional_enforce_equal(
362 &mut cs.ns(|| "X Coordinate Conditional Equality"),
363 &other.x,
364 condition,
365 )?;
366 self.y.conditional_enforce_equal(
367 &mut cs.ns(|| "Y Coordinate Conditional Equality"),
368 &other.y,
369 condition,
370 )?;
371 Ok(())
372 }
373
374 fn cost() -> usize {
375 2 * <F as ConditionalEqGadget<ConstraintF>>::cost()
376 }
377}
378
379impl<P, ConstraintF, F> NEqGadget<ConstraintF> for AffineGadget<P, ConstraintF, F>
380where
381 P: SWModelParameters,
382 ConstraintF: Field,
383 F: FieldGadget<P::BaseField, ConstraintF>,
384{
385 #[inline]
386 fn enforce_not_equal<CS: ConstraintSystem<ConstraintF>>(
387 &self,
388 mut cs: CS,
389 other: &Self,
390 ) -> Result<(), SynthesisError> {
391 self.x
392 .enforce_not_equal(&mut cs.ns(|| "X Coordinate Inequality"), &other.x)?;
393 self.y
394 .enforce_not_equal(&mut cs.ns(|| "Y Coordinate Inequality"), &other.y)?;
395 Ok(())
396 }
397
398 fn cost() -> usize {
399 2 * <F as NEqGadget<ConstraintF>>::cost()
400 }
401}
402
403impl<P, ConstraintF, F> AllocGadget<SWProjective<P>, ConstraintF> for AffineGadget<P, ConstraintF, F>
404where
405 P: SWModelParameters,
406 ConstraintF: Field,
407 F: FieldGadget<P::BaseField, ConstraintF>,
408{
409 #[inline]
410 fn alloc<FN, T, CS: ConstraintSystem<ConstraintF>>(
411 mut cs: CS,
412 value_gen: FN,
413 ) -> Result<Self, SynthesisError>
414 where
415 FN: FnOnce() -> Result<T, SynthesisError>,
416 T: Borrow<SWProjective<P>>,
417 {
418 let (x, y) = match value_gen() {
419 Ok(ge) => {
420 let ge = ge.borrow().into_affine();
421 (Ok(ge.x), Ok(ge.y))
422 },
423 _ => (
424 Err(SynthesisError::AssignmentMissing),
425 Err(SynthesisError::AssignmentMissing),
426 ),
427 };
428
429 // Perform on-curve check.
430 let b = P::COEFF_B;
431 let a = P::COEFF_A;
432
433 let x = F::alloc(&mut cs.ns(|| "x"), || x)?;
434 let y = F::alloc(&mut cs.ns(|| "y"), || y)?;
435
436 // Check that y^2 = x^3 + ax +b
437 // We do this by checking that y^2 - b = x * (x^2 +a)
438 let x2 = x.square(&mut cs.ns(|| "x^2"))?;
439 let y2 = y.square(&mut cs.ns(|| "y^2"))?;
440
441 let x2_plus_a = x2.add_constant(cs.ns(|| "x^2 + a"), &a)?;
442 let y2_minus_b = y2.add_constant(cs.ns(|| "y^2 - b"), &b.neg())?;
443
444 x2_plus_a.mul_equals(cs.ns(|| "on curve check"), &x, &y2_minus_b)?;
445
446 Ok(Self::new(x, y))
447 }
448
449 #[inline]
450 fn alloc_checked<FN, T, CS: ConstraintSystem<ConstraintF>>(
451 mut cs: CS,
452 value_gen: FN,
453 ) -> Result<Self, SynthesisError>
454 where
455 FN: FnOnce() -> Result<T, SynthesisError>,
456 T: Borrow<SWProjective<P>>,
457 {
458 let cofactor_weight = BitIterator::new(P::COFACTOR).filter(|b| *b).count();
459 // If we multiply by r, we actually multiply by r - 2.
460 let r_minus_1 = (-P::ScalarField::one()).into_repr();
461 let r_weight = BitIterator::new(&r_minus_1).filter(|b| *b).count();
462
463 // We pick the most efficient method of performing the prime order check:
464 // If the cofactor has lower hamming weight than the scalar field's modulus,
465 // we first multiply by the inverse of the cofactor, and then, after allocating,
466 // multiply by the cofactor. This ensures the resulting point has no cofactors
467 //
468 // Else, we multiply by the scalar field's modulus and ensure that the result
469 // is zero.
470 if cofactor_weight < r_weight {
471 let ge = Self::alloc(cs.ns(|| "Alloc checked"), || {
472 value_gen().map(|ge| {
473 ge.borrow()
474 .into_affine()
475 .mul_by_cofactor_inv()
476 .into_projective()
477 })
478 })?;
479 let mut seen_one = false;
480 let mut result = Self::zero(cs.ns(|| "result"))?;
481 for (i, b) in BitIterator::new(P::COFACTOR).enumerate() {
482 let mut cs = cs.ns(|| format!("Iteration {}", i));
483
484 let old_seen_one = seen_one;
485 if seen_one {
486 result.double_in_place(cs.ns(|| "Double"))?;
487 } else {
488 seen_one = b;
489 }
490
491 if b {
492 result = if old_seen_one {
493 result.add(cs.ns(|| "Add"), &ge)?
494 } else {
495 ge.clone()
496 };
497 }
498 }
499 Ok(result)
500 } else {
501 let ge = Self::alloc(cs.ns(|| "Alloc checked"), value_gen)?;
502 let mut seen_one = false;
503 let mut result = Self::zero(cs.ns(|| "result"))?;
504 // Returns bits in big-endian order
505 for (i, b) in BitIterator::new(r_minus_1).enumerate() {
506 let mut cs = cs.ns(|| format!("Iteration {}", i));
507
508 let old_seen_one = seen_one;
509 if seen_one {
510 result.double_in_place(cs.ns(|| "Double"))?;
511 } else {
512 seen_one = b;
513 }
514
515 if b {
516 result = if old_seen_one {
517 result.add(cs.ns(|| "Add"), &ge)?
518 } else {
519 ge.clone()
520 };
521 }
522 }
523 let neg_ge = ge.negate(cs.ns(|| "Negate ge"))?;
524 neg_ge.enforce_equal(cs.ns(|| "Check equals"), &result)?;
525 Ok(ge)
526 }
527 }
528
529 #[inline]
530 fn alloc_input<FN, T, CS: ConstraintSystem<ConstraintF>>(
531 mut cs: CS,
532 value_gen: FN,
533 ) -> Result<Self, SynthesisError>
534 where
535 FN: FnOnce() -> Result<T, SynthesisError>,
536 T: Borrow<SWProjective<P>>,
537 {
538 let (x, y) = match value_gen() {
539 Ok(ge) => {
540 let ge = ge.borrow().into_affine();
541 (Ok(ge.x), Ok(ge.y))
542 },
543 _ => (
544 Err(SynthesisError::AssignmentMissing),
545 Err(SynthesisError::AssignmentMissing),
546 ),
547 };
548
549 let b = P::COEFF_B;
550 let a = P::COEFF_A;
551
552 let x = F::alloc_input(&mut cs.ns(|| "x"), || x)?;
553 let y = F::alloc_input(&mut cs.ns(|| "y"), || y)?;
554
555 // Check that y^2 = x^3 + ax +b
556 // We do this by checking that y^2 - b = x * (x^2 +a)
557 let x2 = x.square(&mut cs.ns(|| "x^2"))?;
558 let y2 = y.square(&mut cs.ns(|| "y^2"))?;
559
560 let x2_plus_a = x2.add_constant(cs.ns(|| "x^2 + a"), &a)?;
561 let y2_minus_b = y2.add_constant(cs.ns(|| "y^2 - b"), &b.neg())?;
562
563 x2_plus_a.mul_equals(cs.ns(|| "on curve check"), &x, &y2_minus_b)?;
564
565 Ok(Self::new(x, y))
566 }
567}
568
569impl<P, ConstraintF, F> ToBitsGadget<ConstraintF> for AffineGadget<P, ConstraintF, F>
570where
571 P: SWModelParameters,
572 ConstraintF: Field,
573 F: FieldGadget<P::BaseField, ConstraintF>,
574{
575 fn to_bits<CS: ConstraintSystem<ConstraintF>>(&self, mut cs: CS) -> Result<Vec<Boolean>, SynthesisError> {
576 let mut x_bits = self.x.to_bits(&mut cs.ns(|| "X Coordinate To Bits"))?;
577 let y_bits = self.y.to_bits(&mut cs.ns(|| "Y Coordinate To Bits"))?;
578 x_bits.extend_from_slice(&y_bits);
579 Ok(x_bits)
580 }
581
582 fn to_bits_strict<CS: ConstraintSystem<ConstraintF>>(
583 &self,
584 mut cs: CS,
585 ) -> Result<Vec<Boolean>, SynthesisError> {
586 let mut x_bits = self
587 .x
588 .to_bits_strict(&mut cs.ns(|| "X Coordinate To Bits"))?;
589 let y_bits = self
590 .y
591 .to_bits_strict(&mut cs.ns(|| "Y Coordinate To Bits"))?;
592 x_bits.extend_from_slice(&y_bits);
593
594 Ok(x_bits)
595 }
596}
597
598impl<P, ConstraintF, F> ToBytesGadget<ConstraintF> for AffineGadget<P, ConstraintF, F>
599where
600 P: SWModelParameters,
601 ConstraintF: Field,
602 F: FieldGadget<P::BaseField, ConstraintF>,
603{
604 fn to_bytes<CS: ConstraintSystem<ConstraintF>>(&self, mut cs: CS) -> Result<Vec<UInt8>, SynthesisError> {
605 let mut x_bytes = self.x.to_bytes(&mut cs.ns(|| "X Coordinate To Bytes"))?;
606 let y_bytes = self.y.to_bytes(&mut cs.ns(|| "Y Coordinate To Bytes"))?;
607 x_bytes.extend_from_slice(&y_bytes);
608 Ok(x_bytes)
609 }
610
611 fn to_bytes_strict<CS: ConstraintSystem<ConstraintF>>(
612 &self,
613 mut cs: CS,
614 ) -> Result<Vec<UInt8>, SynthesisError> {
615 let mut x_bytes = self
616 .x
617 .to_bytes_strict(&mut cs.ns(|| "X Coordinate To Bytes"))?;
618 let y_bytes = self
619 .y
620 .to_bytes_strict(&mut cs.ns(|| "Y Coordinate To Bytes"))?;
621 x_bytes.extend_from_slice(&y_bytes);
622
623 Ok(x_bytes)
624 }
625}