2020-09-07 09:22:25 -07:00
|
|
|
//! This module contains an implementation of the polynomial commitment scheme
|
|
|
|
//! described in the [Halo][halo] paper.
|
|
|
|
//!
|
|
|
|
//! [halo]: https://eprint.iacr.org/2019/1021
|
|
|
|
|
2020-09-12 11:55:48 -07:00
|
|
|
use super::{Coeff, LagrangeCoeff, Polynomial};
|
2021-02-22 14:50:01 -08:00
|
|
|
use crate::arithmetic::{
|
|
|
|
best_fft, best_multiexp, parallelize, CurveAffine, CurveExt, FieldExt, Group,
|
|
|
|
};
|
2021-09-30 15:05:15 -07:00
|
|
|
use crate::helpers::CurveRead;
|
2020-11-12 16:08:08 -08:00
|
|
|
|
|
|
|
use ff::{Field, PrimeField};
|
2021-02-22 14:50:01 -08:00
|
|
|
use group::{prime::PrimeCurveAffine, Curve, Group as _};
|
2020-12-01 14:40:54 -08:00
|
|
|
use std::ops::{Add, AddAssign, Mul, MulAssign};
|
2020-09-07 09:22:25 -07:00
|
|
|
|
2020-10-13 07:16:20 -07:00
|
|
|
mod msm;
|
2020-09-07 09:22:25 -07:00
|
|
|
mod prover;
|
|
|
|
mod verifier;
|
|
|
|
|
2020-10-13 07:16:20 -07:00
|
|
|
pub use msm::MSM;
|
2020-12-21 11:34:51 -08:00
|
|
|
pub use prover::create_proof;
|
|
|
|
pub use verifier::{verify_proof, Accumulator, Guard};
|
2020-09-25 08:11:37 -07:00
|
|
|
|
2021-01-13 01:39:10 -08:00
|
|
|
use std::io;
|
|
|
|
|
2020-09-07 09:22:25 -07:00
|
|
|
/// These are the public parameters for the polynomial commitment scheme.
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct Params<C: CurveAffine> {
|
|
|
|
pub(crate) k: u32,
|
|
|
|
pub(crate) n: u64,
|
|
|
|
pub(crate) g: Vec<C>,
|
|
|
|
pub(crate) g_lagrange: Vec<C>,
|
2022-02-09 12:36:49 -08:00
|
|
|
pub(crate) w: C,
|
2020-12-21 11:34:51 -08:00
|
|
|
pub(crate) u: C,
|
2020-10-13 07:16:20 -07:00
|
|
|
}
|
|
|
|
|
2020-09-07 09:22:25 -07:00
|
|
|
impl<C: CurveAffine> Params<C> {
|
|
|
|
/// Initializes parameters for the curve, given a random oracle to draw
|
|
|
|
/// points from.
|
2021-02-22 14:50:01 -08:00
|
|
|
pub fn new(k: u32) -> Self {
|
2020-09-07 09:22:25 -07:00
|
|
|
// This is usually a limitation on the curve, but we also want 32-bit
|
|
|
|
// architectures to be supported.
|
|
|
|
assert!(k < 32);
|
2021-01-12 16:12:12 -08:00
|
|
|
|
|
|
|
// In src/arithmetic/fields.rs we ensure that usize is at least 32 bits.
|
2020-09-07 09:22:25 -07:00
|
|
|
|
|
|
|
let n: u64 = 1 << k;
|
|
|
|
|
2021-02-22 14:50:01 -08:00
|
|
|
let g_projective = {
|
2020-09-07 09:22:25 -07:00
|
|
|
let mut g = Vec::with_capacity(n as usize);
|
2021-02-22 14:50:01 -08:00
|
|
|
g.resize(n as usize, C::Curve::identity());
|
|
|
|
|
2020-09-07 09:22:25 -07:00
|
|
|
parallelize(&mut g, move |g, start| {
|
2021-02-22 15:13:11 -08:00
|
|
|
let hasher = C::CurveExt::hash_to_curve("Halo2-Parameters");
|
2020-12-23 15:09:17 -08:00
|
|
|
|
|
|
|
for (i, g) in g.iter_mut().enumerate() {
|
2021-02-22 15:13:11 -08:00
|
|
|
let i = (i + start) as u32;
|
|
|
|
|
|
|
|
let mut message = [0u8; 5];
|
|
|
|
message[1..5].copy_from_slice(&i.to_le_bytes());
|
2020-12-23 15:09:17 -08:00
|
|
|
|
2021-02-22 15:13:11 -08:00
|
|
|
*g = hasher(&message);
|
2020-09-07 09:22:25 -07:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
g
|
|
|
|
};
|
|
|
|
|
2021-02-22 14:50:01 -08:00
|
|
|
let g = {
|
|
|
|
let mut g = vec![C::identity(); n as usize];
|
|
|
|
parallelize(&mut g, |g, starts| {
|
|
|
|
C::Curve::batch_normalize(&g_projective[starts..(starts + g.len())], g);
|
|
|
|
});
|
|
|
|
g
|
|
|
|
};
|
|
|
|
|
2020-09-07 09:22:25 -07:00
|
|
|
// Let's evaluate all of the Lagrange basis polynomials
|
|
|
|
// using an inverse FFT.
|
2021-02-22 11:02:53 -08:00
|
|
|
let mut alpha_inv = <<C as PrimeCurveAffine>::Curve as Group>::Scalar::ROOT_OF_UNITY_INV;
|
2020-09-07 09:22:25 -07:00
|
|
|
for _ in k..C::Scalar::S {
|
|
|
|
alpha_inv = alpha_inv.square();
|
|
|
|
}
|
2021-02-22 14:50:01 -08:00
|
|
|
let mut g_lagrange_projective = g_projective;
|
2020-09-07 09:22:25 -07:00
|
|
|
best_fft(&mut g_lagrange_projective, alpha_inv, k);
|
|
|
|
let minv = C::Scalar::TWO_INV.pow_vartime(&[k as u64, 0, 0, 0]);
|
|
|
|
parallelize(&mut g_lagrange_projective, |g, _| {
|
|
|
|
for g in g.iter_mut() {
|
|
|
|
*g *= minv;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
let g_lagrange = {
|
2021-02-22 10:39:14 -08:00
|
|
|
let mut g_lagrange = vec![C::identity(); n as usize];
|
2020-09-07 09:22:25 -07:00
|
|
|
parallelize(&mut g_lagrange, |g_lagrange, starts| {
|
2021-02-22 10:39:14 -08:00
|
|
|
C::Curve::batch_normalize(
|
2020-09-07 09:22:25 -07:00
|
|
|
&g_lagrange_projective[starts..(starts + g_lagrange.len())],
|
|
|
|
g_lagrange,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
drop(g_lagrange_projective);
|
|
|
|
g_lagrange
|
|
|
|
};
|
|
|
|
|
2021-02-22 15:13:11 -08:00
|
|
|
let hasher = C::CurveExt::hash_to_curve("Halo2-Parameters");
|
2022-02-09 12:36:49 -08:00
|
|
|
let w = hasher(&[1]).to_affine();
|
2021-02-22 15:13:11 -08:00
|
|
|
let u = hasher(&[2]).to_affine();
|
2020-09-07 09:22:25 -07:00
|
|
|
|
|
|
|
Params {
|
|
|
|
k,
|
|
|
|
n,
|
|
|
|
g,
|
|
|
|
g_lagrange,
|
2022-02-09 12:36:49 -08:00
|
|
|
w,
|
2020-12-21 11:34:51 -08:00
|
|
|
u,
|
2020-09-07 09:22:25 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// This computes a commitment to a polynomial described by the provided
|
|
|
|
/// slice of coefficients. The commitment will be blinded by the blinding
|
|
|
|
/// factor `r`.
|
2021-02-22 10:39:14 -08:00
|
|
|
pub fn commit(&self, poly: &Polynomial<C::Scalar, Coeff>, r: Blind<C::Scalar>) -> C::Curve {
|
2020-09-07 09:22:25 -07:00
|
|
|
let mut tmp_scalars = Vec::with_capacity(poly.len() + 1);
|
|
|
|
let mut tmp_bases = Vec::with_capacity(poly.len() + 1);
|
|
|
|
|
|
|
|
tmp_scalars.extend(poly.iter());
|
|
|
|
tmp_scalars.push(r.0);
|
|
|
|
|
|
|
|
tmp_bases.extend(self.g.iter());
|
2022-02-09 12:36:49 -08:00
|
|
|
tmp_bases.push(self.w);
|
2020-09-07 09:22:25 -07:00
|
|
|
|
|
|
|
best_multiexp::<C>(&tmp_scalars, &tmp_bases)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// This commits to a polynomial using its evaluations over the $2^k$ size
|
|
|
|
/// evaluation domain. The commitment will be blinded by the blinding factor
|
|
|
|
/// `r`.
|
|
|
|
pub fn commit_lagrange(
|
|
|
|
&self,
|
|
|
|
poly: &Polynomial<C::Scalar, LagrangeCoeff>,
|
|
|
|
r: Blind<C::Scalar>,
|
2021-02-22 10:39:14 -08:00
|
|
|
) -> C::Curve {
|
2020-09-07 09:22:25 -07:00
|
|
|
let mut tmp_scalars = Vec::with_capacity(poly.len() + 1);
|
|
|
|
let mut tmp_bases = Vec::with_capacity(poly.len() + 1);
|
|
|
|
|
|
|
|
tmp_scalars.extend(poly.iter());
|
|
|
|
tmp_scalars.push(r.0);
|
|
|
|
|
|
|
|
tmp_bases.extend(self.g_lagrange.iter());
|
2022-02-09 12:36:49 -08:00
|
|
|
tmp_bases.push(self.w);
|
2020-09-07 09:22:25 -07:00
|
|
|
|
|
|
|
best_multiexp::<C>(&tmp_scalars, &tmp_bases)
|
|
|
|
}
|
2020-09-12 11:55:48 -07:00
|
|
|
|
|
|
|
/// Generates an empty multiscalar multiplication struct using the
|
|
|
|
/// appropriate params.
|
2020-09-13 08:07:05 -07:00
|
|
|
pub fn empty_msm(&self) -> MSM<C> {
|
2020-10-13 07:16:20 -07:00
|
|
|
MSM::new(self)
|
2020-09-12 11:55:48 -07:00
|
|
|
}
|
2020-09-15 09:44:56 -07:00
|
|
|
|
|
|
|
/// Getter for g generators
|
|
|
|
pub fn get_g(&self) -> Vec<C> {
|
|
|
|
self.g.clone()
|
|
|
|
}
|
2021-01-13 01:39:10 -08:00
|
|
|
|
|
|
|
/// Writes params to a buffer.
|
|
|
|
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
|
|
writer.write_all(&self.k.to_le_bytes())?;
|
|
|
|
for g_element in &self.g {
|
2021-02-22 11:02:53 -08:00
|
|
|
writer.write_all(g_element.to_bytes().as_ref())?;
|
2021-01-13 01:39:10 -08:00
|
|
|
}
|
|
|
|
for g_lagrange_element in &self.g_lagrange {
|
2021-02-22 11:02:53 -08:00
|
|
|
writer.write_all(g_lagrange_element.to_bytes().as_ref())?;
|
2021-01-13 01:39:10 -08:00
|
|
|
}
|
2022-02-09 12:36:49 -08:00
|
|
|
writer.write_all(self.w.to_bytes().as_ref())?;
|
2021-02-22 11:02:53 -08:00
|
|
|
writer.write_all(self.u.to_bytes().as_ref())?;
|
2021-01-13 01:39:10 -08:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Reads params from a buffer.
|
|
|
|
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
|
|
|
let mut k = [0u8; 4];
|
|
|
|
reader.read_exact(&mut k[..])?;
|
|
|
|
let k = u32::from_le_bytes(k);
|
|
|
|
|
2021-01-21 20:31:27 -08:00
|
|
|
let n: u64 = 1 << k;
|
2021-01-13 01:39:10 -08:00
|
|
|
|
2021-01-21 15:40:25 -08:00
|
|
|
let g: Vec<_> = (0..n).map(|_| C::read(reader)).collect::<Result<_, _>>()?;
|
|
|
|
let g_lagrange: Vec<_> = (0..n).map(|_| C::read(reader)).collect::<Result<_, _>>()?;
|
2021-01-13 01:39:10 -08:00
|
|
|
|
2022-02-09 12:36:49 -08:00
|
|
|
let w = C::read(reader)?;
|
2021-01-13 01:39:10 -08:00
|
|
|
let u = C::read(reader)?;
|
|
|
|
|
|
|
|
Ok(Params {
|
|
|
|
k,
|
|
|
|
n,
|
|
|
|
g,
|
|
|
|
g_lagrange,
|
2022-02-09 12:36:49 -08:00
|
|
|
w,
|
2021-01-13 01:39:10 -08:00
|
|
|
u,
|
|
|
|
})
|
|
|
|
}
|
2020-09-07 09:22:25 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Wrapper type around a blinding factor.
|
|
|
|
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
|
|
|
pub struct Blind<F>(pub F);
|
|
|
|
|
2020-11-25 11:26:31 -08:00
|
|
|
impl<F: FieldExt> Default for Blind<F> {
|
2020-09-07 09:22:25 -07:00
|
|
|
fn default() -> Self {
|
|
|
|
Blind(F::one())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-25 11:26:31 -08:00
|
|
|
impl<F: FieldExt> Add for Blind<F> {
|
2020-09-07 09:22:25 -07:00
|
|
|
type Output = Self;
|
|
|
|
|
|
|
|
fn add(self, rhs: Blind<F>) -> Self {
|
|
|
|
Blind(self.0 + rhs.0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-25 11:26:31 -08:00
|
|
|
impl<F: FieldExt> Mul for Blind<F> {
|
2020-09-07 09:22:25 -07:00
|
|
|
type Output = Self;
|
|
|
|
|
|
|
|
fn mul(self, rhs: Blind<F>) -> Self {
|
|
|
|
Blind(self.0 * rhs.0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-25 11:26:31 -08:00
|
|
|
impl<F: FieldExt> AddAssign for Blind<F> {
|
2020-09-07 09:22:25 -07:00
|
|
|
fn add_assign(&mut self, rhs: Blind<F>) {
|
|
|
|
self.0 += rhs.0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-25 11:26:31 -08:00
|
|
|
impl<F: FieldExt> MulAssign for Blind<F> {
|
2020-09-07 09:22:25 -07:00
|
|
|
fn mul_assign(&mut self, rhs: Blind<F>) {
|
|
|
|
self.0 *= rhs.0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-25 11:26:31 -08:00
|
|
|
impl<F: FieldExt> AddAssign<F> for Blind<F> {
|
2020-09-07 09:22:25 -07:00
|
|
|
fn add_assign(&mut self, rhs: F) {
|
|
|
|
self.0 += rhs;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-25 11:26:31 -08:00
|
|
|
impl<F: FieldExt> MulAssign<F> for Blind<F> {
|
2020-09-07 09:22:25 -07:00
|
|
|
fn mul_assign(&mut self, rhs: F) {
|
|
|
|
self.0 *= rhs;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2020-12-21 11:34:51 -08:00
|
|
|
fn test_commit_lagrange_epaffine() {
|
2020-09-07 09:22:25 -07:00
|
|
|
const K: u32 = 6;
|
|
|
|
|
2022-01-20 08:16:48 -08:00
|
|
|
use rand_core::OsRng;
|
2021-12-25 05:36:41 -08:00
|
|
|
|
2020-12-21 11:34:51 -08:00
|
|
|
use crate::pasta::{EpAffine, Fq};
|
2020-12-23 15:09:17 -08:00
|
|
|
let params = Params::<EpAffine>::new(K);
|
2020-09-07 09:22:25 -07:00
|
|
|
let domain = super::EvaluationDomain::new(1, K);
|
|
|
|
|
|
|
|
let mut a = domain.empty_lagrange();
|
|
|
|
|
|
|
|
for (i, a) in a.iter_mut().enumerate() {
|
|
|
|
*a = Fq::from(i as u64);
|
|
|
|
}
|
|
|
|
|
|
|
|
let b = domain.lagrange_to_coeff(a.clone());
|
|
|
|
|
2021-12-25 05:36:41 -08:00
|
|
|
let alpha = Blind(Fq::random(OsRng));
|
2020-09-07 09:22:25 -07:00
|
|
|
|
|
|
|
assert_eq!(params.commit(&b, alpha), params.commit_lagrange(&a, alpha));
|
|
|
|
}
|
|
|
|
|
2020-12-21 11:34:51 -08:00
|
|
|
#[test]
|
|
|
|
fn test_commit_lagrange_eqaffine() {
|
|
|
|
const K: u32 = 6;
|
|
|
|
|
2022-01-20 08:16:48 -08:00
|
|
|
use rand_core::OsRng;
|
2021-12-25 05:36:41 -08:00
|
|
|
|
2020-12-21 11:34:51 -08:00
|
|
|
use crate::pasta::{EqAffine, Fp};
|
2020-12-23 15:09:17 -08:00
|
|
|
let params = Params::<EqAffine>::new(K);
|
2020-12-21 11:34:51 -08:00
|
|
|
let domain = super::EvaluationDomain::new(1, K);
|
|
|
|
|
|
|
|
let mut a = domain.empty_lagrange();
|
|
|
|
|
|
|
|
for (i, a) in a.iter_mut().enumerate() {
|
|
|
|
*a = Fp::from(i as u64);
|
|
|
|
}
|
|
|
|
|
|
|
|
let b = domain.lagrange_to_coeff(a.clone());
|
|
|
|
|
2021-12-25 05:36:41 -08:00
|
|
|
let alpha = Blind(Fp::random(OsRng));
|
2020-12-21 11:34:51 -08:00
|
|
|
|
|
|
|
assert_eq!(params.commit(&b, alpha), params.commit_lagrange(&a, alpha));
|
|
|
|
}
|
|
|
|
|
2020-09-07 09:22:25 -07:00
|
|
|
#[test]
|
|
|
|
fn test_opening_proof() {
|
|
|
|
const K: u32 = 6;
|
|
|
|
|
2020-11-12 16:08:08 -08:00
|
|
|
use ff::Field;
|
2022-01-20 08:16:48 -08:00
|
|
|
use rand_core::OsRng;
|
2020-11-12 16:08:08 -08:00
|
|
|
|
2020-09-07 09:22:25 -07:00
|
|
|
use super::{
|
|
|
|
commitment::{Blind, Params},
|
|
|
|
EvaluationDomain,
|
|
|
|
};
|
2021-01-13 01:39:10 -08:00
|
|
|
use crate::arithmetic::{eval_polynomial, FieldExt};
|
2020-12-21 11:34:51 -08:00
|
|
|
use crate::pasta::{EpAffine, Fq};
|
|
|
|
use crate::transcript::{
|
2021-04-30 18:28:50 -07:00
|
|
|
Blake2bRead, Blake2bWrite, Challenge255, Transcript, TranscriptRead, TranscriptWrite,
|
2020-12-21 11:34:51 -08:00
|
|
|
};
|
2020-09-07 09:22:25 -07:00
|
|
|
|
2021-12-25 05:36:41 -08:00
|
|
|
let rng = OsRng;
|
|
|
|
|
2020-12-23 15:09:17 -08:00
|
|
|
let params = Params::<EpAffine>::new(K);
|
2021-01-13 01:39:10 -08:00
|
|
|
let mut params_buffer = vec![];
|
|
|
|
params.write(&mut params_buffer).unwrap();
|
|
|
|
let params: Params<EpAffine> = Params::read::<_>(&mut ¶ms_buffer[..]).unwrap();
|
|
|
|
|
2020-09-07 09:22:25 -07:00
|
|
|
let domain = EvaluationDomain::new(1, K);
|
|
|
|
|
|
|
|
let mut px = domain.empty_coeff();
|
|
|
|
|
|
|
|
for (i, a) in px.iter_mut().enumerate() {
|
|
|
|
*a = Fq::from(i as u64);
|
|
|
|
}
|
|
|
|
|
2021-12-25 05:36:41 -08:00
|
|
|
let blind = Blind(Fq::random(rng));
|
2020-09-07 09:22:25 -07:00
|
|
|
|
|
|
|
let p = params.commit(&px, blind).to_affine();
|
|
|
|
|
2021-05-07 07:21:54 -07:00
|
|
|
let mut transcript = Blake2bWrite::<Vec<u8>, EpAffine, Challenge255<EpAffine>>::init(vec![]);
|
2020-12-21 11:34:51 -08:00
|
|
|
transcript.write_point(p).unwrap();
|
2021-04-12 23:47:25 -07:00
|
|
|
let x = transcript.squeeze_challenge_scalar::<()>();
|
2020-09-07 09:22:25 -07:00
|
|
|
// Evaluate the polynomial
|
2020-12-01 14:34:18 -08:00
|
|
|
let v = eval_polynomial(&px, *x);
|
2020-12-21 11:34:51 -08:00
|
|
|
transcript.write_scalar(v).unwrap();
|
2020-09-07 09:22:25 -07:00
|
|
|
|
2020-12-21 11:34:51 -08:00
|
|
|
let (proof, ch_prover) = {
|
2021-12-25 05:36:41 -08:00
|
|
|
create_proof(¶ms, rng, &mut transcript, &px, blind, *x).unwrap();
|
2020-12-21 11:34:51 -08:00
|
|
|
let ch_prover = transcript.squeeze_challenge();
|
|
|
|
(transcript.finalize(), ch_prover)
|
|
|
|
};
|
|
|
|
|
|
|
|
// Verify the opening proof
|
2021-05-07 07:21:54 -07:00
|
|
|
let mut transcript = Blake2bRead::<&[u8], EpAffine, Challenge255<EpAffine>>::init(&proof[..]);
|
2020-12-21 11:34:51 -08:00
|
|
|
let p_prime = transcript.read_point().unwrap();
|
|
|
|
assert_eq!(p, p_prime);
|
2021-04-12 23:47:25 -07:00
|
|
|
let x_prime = transcript.squeeze_challenge_scalar::<()>();
|
2020-12-21 11:34:51 -08:00
|
|
|
assert_eq!(*x, *x_prime);
|
|
|
|
let v_prime = transcript.read_scalar().unwrap();
|
|
|
|
assert_eq!(v, v_prime);
|
|
|
|
|
|
|
|
let mut commitment_msm = params.empty_msm();
|
|
|
|
commitment_msm.append_term(Field::one(), p);
|
2020-12-23 14:37:34 -08:00
|
|
|
let guard = verify_proof(¶ms, commitment_msm, &mut transcript, *x, v).unwrap();
|
2020-12-21 11:34:51 -08:00
|
|
|
let ch_verifier = transcript.squeeze_challenge();
|
2021-04-22 22:14:42 -07:00
|
|
|
assert_eq!(*ch_prover, *ch_verifier);
|
2020-12-21 11:34:51 -08:00
|
|
|
|
|
|
|
// Test guard behavior prior to checking another proof
|
|
|
|
{
|
|
|
|
// Test use_challenges()
|
|
|
|
let msm_challenges = guard.clone().use_challenges();
|
|
|
|
assert!(msm_challenges.eval());
|
|
|
|
|
|
|
|
// Test use_g()
|
|
|
|
let g = guard.compute_g();
|
|
|
|
let (msm_g, _accumulator) = guard.clone().use_g(g);
|
|
|
|
assert!(msm_g.eval());
|
2020-09-07 09:22:25 -07:00
|
|
|
}
|
|
|
|
}
|