-
Notifications
You must be signed in to change notification settings - Fork 48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add LPN and GGM related #54
Add LPN and GGM related #54
Conversation
Tried different ways to optimize LPN computation with multi threads. The problem: Given two F_{2^128} vectors Since the dimensions of Optimizations:
My experiments show that 1.a (1.b) + 2.a outperform others. |
Is this about ready for review? |
Not yet, I am optimizing ggm, probably will finish in these days. |
The computation of generation (and reconstruction) of ggm tree in each level is highly parallel. Try to optimize this part in two ways.
So, I will keep the original implementation, and add comments on public functions. |
It is ready for review. :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, thank you!
I mostly provided some feedback around formatting conventions and idiomatic Rust things. Looks good otherwise
You may want to rebase on #59, see the relevant comment below.
mpz-core/src/lpn.rs
Outdated
// The seed to generate the random sparse matrix A. | ||
seed: Block, | ||
|
||
// The length of the secret, i.e., x. | ||
k: u32, | ||
|
||
// A mask to optimize reduction operation. | ||
mask: u32, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use doc comments for these and they will be shown with cargo doc --document-private-items
// The seed to generate the random sparse matrix A. | |
seed: Block, | |
// The length of the secret, i.e., x. | |
k: u32, | |
// A mask to optimize reduction operation. | |
mask: u32, | |
/// The seed to generate the random sparse matrix A. | |
seed: Block, | |
/// The length of the secret, i.e., x. | |
k: u32, | |
/// A mask to optimize reduction operation. | |
mask: u32, |
mpz-core/src/lpn.rs
Outdated
/// A struct related to LPN. | ||
/// The `seed` defines a sparse binary matrix `A` with at most `D` non-zero values in each row. | ||
/// `A` - is a binary matrix with `k` columns and `n` rows. The concrete number of `n` is determined by the input length. `A` will be generated on-the-fly. | ||
/// `x` - is a `F_{2^128}` vector with length `k`. | ||
/// `e` - is a `F_{2^128}` vector with length `n`. | ||
/// Given a vector `x` and `e`, compute `y = Ax + e`. | ||
/// Note that in the standard LPN problem, `x` is a binary vector, `e` is a sparse binary vector. The way we difined here is a more generic way in term of computing `y`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rustdocs need an extra newline
/// A struct related to LPN. | |
/// The `seed` defines a sparse binary matrix `A` with at most `D` non-zero values in each row. | |
/// `A` - is a binary matrix with `k` columns and `n` rows. The concrete number of `n` is determined by the input length. `A` will be generated on-the-fly. | |
/// `x` - is a `F_{2^128}` vector with length `k`. | |
/// `e` - is a `F_{2^128}` vector with length `n`. | |
/// Given a vector `x` and `e`, compute `y = Ax + e`. | |
/// Note that in the standard LPN problem, `x` is a binary vector, `e` is a sparse binary vector. The way we difined here is a more generic way in term of computing `y`. | |
/// A struct related to LPN. | |
/// | |
/// The `seed` defines a sparse binary matrix `A` with at most `D` non-zero values in each row. | |
/// | |
/// Given a vector `x` and `e`, compute `y = Ax + e`, where: | |
/// | |
/// * `A` - is a binary matrix with `k` columns and `n` rows. The concrete number of `n` is determined by the input length. `A` will be generated on-the-fly. | |
/// * `x` - is a `F_{2^128}` vector with length `k`. | |
/// * `e` - is a `F_{2^128}` vector with length `n`. | |
/// | |
/// Note that in the standard LPN problem, `x` is a binary vector, `e` is a sparse binary vector. The way we defined here is a more generic way in term of computing `y`. |
mpz-core/src/lpn.rs
Outdated
|
||
use crate::{prp::Prp, Block}; | ||
use rayon::prelude::*; | ||
/// A struct related to LPN. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a better description of this? An "LPN encoder" perhaps?
mpz-core/src/lpn.rs
Outdated
} | ||
|
||
impl<const D: usize> Lpn<D> { | ||
/// New an LPN instance |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// New an LPN instance | |
/// Creates a new LPN instance. | |
/// | |
/// # Arguments | |
/// | |
/// * `seed` - The seed to generate the random sparse matrix A. | |
/// * `k` - The length of the secret, i.e., `|x|`. |
mpz-core/src/ggm_tree.rs
Outdated
assert!(k1.len() == self.depth - 1); | ||
assert!(tree.len() == 1 << (self.depth)); | ||
assert!(k0.len() == self.depth); | ||
assert!(k1.len() == self.depth); | ||
let mut buf = vec![Block::ZERO; 8]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can avoid this heap allocation
let mut buf = vec![Block::ZERO; 8]; | |
let mut buf = [Block::ZERO; 8]; |
mpz-core/src/ggm_tree.rs
Outdated
/// Input : `seed` - a seed. | ||
/// Output: `tree` - a GGM (binary tree) `tree`, with size `2^{depth}`. | ||
/// Output: `k0` - XORs of all the left-node values in each level, with size `depth`. | ||
/// Output: `k1`- XORs of all the right-node values in each level, with size `depth`. | ||
/// This implementation is adapted from EMP Toolkit. | ||
pub fn gen(&self, seed: Block, tree: &mut [Block], k0: &mut [Block], k1: &mut [Block]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Input : `seed` - a seed. | |
/// Output: `tree` - a GGM (binary tree) `tree`, with size `2^{depth}`. | |
/// Output: `k0` - XORs of all the left-node values in each level, with size `depth`. | |
/// Output: `k1`- XORs of all the right-node values in each level, with size `depth`. | |
/// This implementation is adapted from EMP Toolkit. | |
pub fn gen(&self, seed: Block, tree: &mut [Block], k0: &mut [Block], k1: &mut [Block]) { | |
/// Generate a GGM tree in-place. | |
/// | |
/// # Arguments | |
/// | |
/// * `seed` - a seed. | |
/// * `tree` - the destination to write the GGM (binary tree) `tree`, with size `2^{depth}`. | |
/// * `k0` - XORs of all the left-node values in each level, with size `depth`. | |
/// * `k1`- XORs of all the right-node values in each level, with size `depth`. | |
pub fn gen(&self, seed: Block, tree: &mut [Block], k0: &mut [Block], k1: &mut [Block]) { | |
// This implementation is adapted from EMP Toolkit. |
mpz-core/src/ggm_tree.rs
Outdated
/// Reconstruct the GGM tree except the value in a given position. | ||
/// Input : `k` - a slice of blocks with length `depth`, the values of k are chosen via OT from k0 and k1. For the i-th value, if alpha[i] == 1, k[i] = k1[i]; else k[i] = k0[i]. | ||
/// Input : `alpha` - a slice of bits with length `depth`. | ||
/// Output : `tree` - the ggm tree, except `tree[pos] == Block::ZERO`. The bit decomposition of `pos` is the complement of `alpha`. I.e., `pos[i] = 1 xor alpha[i]`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Reconstruct the GGM tree except the value in a given position. | |
/// Input : `k` - a slice of blocks with length `depth`, the values of k are chosen via OT from k0 and k1. For the i-th value, if alpha[i] == 1, k[i] = k1[i]; else k[i] = k0[i]. | |
/// Input : `alpha` - a slice of bits with length `depth`. | |
/// Output : `tree` - the ggm tree, except `tree[pos] == Block::ZERO`. The bit decomposition of `pos` is the complement of `alpha`. I.e., `pos[i] = 1 xor alpha[i]`. | |
/// Reconstruct the GGM tree except the value in a given position. | |
/// | |
/// This reconstructs the GGM tree entirely except `tree[pos] == Block::ZERO`. The bit decomposition of `pos` is the complement of `alpha`. i.e., `pos[i] = 1 xor alpha[i]`. | |
/// | |
/// # Arguments | |
/// | |
/// * `k` - a slice of blocks with length `depth`, the values of k are chosen via OT from k0 and k1. For the i-th value, if alpha[i] == 1, k[i] = k1[i]; else k[i] = k0[i]. | |
/// * `alpha` - a slice of bits with length `depth`. | |
/// * `tree` - the destination to write the GGM tree. |
mpz-core/src/ggm_tree.rs
Outdated
return; | ||
} | ||
|
||
let mut buf = vec![Block::ZERO; 8]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let mut buf = vec![Block::ZERO; 8]; | |
let mut buf = [Block::ZERO; 8]; |
mpz-core/src/prp.rs
Outdated
pub struct Prp(AesEncryptor); | ||
|
||
impl Prp { | ||
/// New an instance of Prp. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// New an instance of Prp. | |
/// Creates a new Prp instance. |
/// | ||
/// `e` - is a `F_{2^128}` vector with length `n`. | ||
/// | ||
/// Note that in the standard LPN problem, `x` is a binary vector, `e` is a sparse binary vector. The way we difined here is a more generic way in term of computing `y`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@xiangxiecrypto , would it be possible to add some links to literature which justifies mixing a binary matrix with non-binary vectors? I could not find it with a simple web search.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a common step in PCG-style COT. You can find the intuition in the Ferret paper (page 11) and the references in it.
Note that we can embed a bit into a Block
to support binary matrix with binary vectors.
@xiangxiecrypto , would it be possible to add some links to literature which justifies mixing a binary matrix with non-binary vectors? I could not find it with a simple web search.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@xiangxiecrypto , thanks, I was worried because I read in
https://eprint.iacr.org/2017/617
"""
How to sample matrices with large dual distance? We suggest to sample a d-sparse matrix M ∈ Fm×k in two steps. First, choose the locations of the non-zero entries of the matrix (e.g., by selecting a random set of d entries per row), and then fill them with random field elements.
"""
since it says "random field elements" I was assuming that we can't just use "1" as your code does but we must use an element from F_2^128.
Maybe this does not apply to our case, idk. If it's not too laborious to explain, could you shed some light why using "1" instead of a random field element is ok?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@xiangxiecrypto , thanks, I was worried because I read in https://eprint.iacr.org/2017/617 """ How to sample matrices with large dual distance? We suggest to sample a d-sparse matrix M ∈ Fm×k in two steps. First, choose the locations of the non-zero entries of the matrix (e.g., by selecting a random set of d entries per row), and then fill them with random field elements. """ since it says "random field elements" I was assuming that we can't just use "1" as your code does but we must use an element from F_2^128.
Maybe this does not apply to our case, idk. If it's not too laborious to explain, could you shed some light why using "1" instead of a random field element is ok?
The security here relies on LPN over F_2, therefore you only need to choose bits.
The reason the function also applies to F_2^128 is that the protocol requires these operations (not related to security assumption). See page 11 in the Ferret paper.
use rayon::prelude::*; | ||
/// An LPN encoder. | ||
/// | ||
/// The `seed` defines a sparse binary matrix `A` with at most `D` non-zero values in each row. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@xiangxiecrypto , is at most correct here? Shouldn't it be exactly D non-zero values?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is at most. Recall that the process is choosing d
positions at random in a row, and filling the d
positions with random elements (in F_2). You do not need to fill all d
positions with 1
.
/// Panics if `x.len() !=k` or `y.len() != n`. | ||
pub fn compute(&self, y: &mut [Block], x: &[Block]) { | ||
assert_eq!(x.len() as u32, self.k); | ||
assert!(x.len() >= D); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@xiangxiecrypto , shouldn't we have here a specific algorithm which bounds the minimal amount of rows/columns depending on D?
As mentioned in https://eprint.iacr.org/2017/617
"Then, for 80-bit security and d = 10 it turns out that we will need approximately k = 182 columns and k 2 rows, while for 100-bit security we need k = 240."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The security also depends on k
and Hamming weight of the error vector. I will specify a new struct to contain these parameters (together with others) when I implement the COT protocol. We could simply focus on D = 10
here.
It is only for computation purpose here, maybe I should change the name to LpnEncoder
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gw, thank you.
I have not fully reviewed reconstruct_layer
. will do some time next week.
/// # Panics | ||
/// | ||
/// Panics if `x.len() !=k` or `y.len() != n`. | ||
pub fn compute(&self, y: &mut [Block], x: &[Block]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are your thoughts about having one common method for computing rows regardless of row count? The code would look like this:
/// The size of one batch of matrix row computation.
///
/// Experiments show that 4 gives the best performance with rayon.
const BATCH_SIZE: usize = 4;
pub fn compute(&self, y: &mut [Block], x: &[Block]) {
assert_eq!(x.len() as u32, self.k);
assert!(x.len() >= D);
let prp = Prp::new(self.seed);
// how many rows will be processed in batches
let rows_in_batches = y.len() - (y.len() % BATCH_SIZE);
cfg_if::cfg_if! {
if #[cfg(feature = "rayon")]{
let mut iter = y.par_chunks_exact_mut(BATCH_SIZE);
}else{
let mut iter = y.par_chunks_exact_mut(BATCH_SIZE);
}
}
let remaining_rows = iter.take_remainder();
iter.enumerate().for_each(|(i, y)| {
self.compute_rows(y, x, i * BATCH_SIZE, &prp);
});
// process remaining rows, if any
self.compute_rows(remaining_rows, x, rows_in_batches, &prp);
}
/// Computes as many rows as there are elements in `y`, placing the result in `y`.
#[inline]
fn compute_rows(&self, y: &mut [Block], x: &[Block], pos: usize, prp: &Prp) {
// How many `Blocks` needed to derive random u32-sized indices for one row
let block_cnt = (D + 4 - 1) / 4;
let mut indices = (0..y.len())
.flat_map(|offset| {
(0..block_cnt).map(move |i| {
Block::from(bytemuck::cast::<_, [u8; 16]>([
(pos + offset) as u64,
i as u64,
]))
})
})
.collect::<Vec<Block>>();
// derive pseudo-random u32-sized indices
prp.permute_block_inplace(&mut indices);
let indices = bytemuck::cast_slice_mut::<_, u32>(&mut indices);
for (row, indices) in y.iter_mut().zip(indices.chunks_exact_mut(block_cnt * 4)) {
// We only need D indices for one row, ignore any extra ones
for ind in indices.iter_mut().take(D) {
// reduce the index to be in the [0, k) range
*ind &= self.mask;
*ind = if *ind >= self.k { *ind - self.k } else { *ind };
*row ^= x[*ind as usize];
}
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point, let me check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that the test failed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see.
Looking at this again:
earlier in compute_four_rows_indep
there were exactly 10 PRG calls for 4 rows, i.e. 2.5 calls/row
This new code makes 3 calls/row.
This is 20% overhead which is too much.
Makes sense to continue with the compute_four_rows_indep
approach.
mpz-core/src/ggm_tree.rs
Outdated
/// # Arguments | ||
/// | ||
/// * `seed` - a seed. | ||
/// * `tree` - the destination of write the GGM (binary tree) `tree`, with size `2^{depth}`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// * `tree` - the destination of write the GGM (binary tree) `tree`, with size `2^{depth}`. | |
/// * `tree` - the destination to write the GGM (binary tree) `tree`, with size `2^{depth}`. |
@@ -37,7 +41,7 @@ impl GgmTree { | |||
k1[1] = buf[1] ^ buf[3]; | |||
tree[0..4].copy_from_slice(&buf[0..4]); | |||
|
|||
for h in 2..self.depth - 1 { | |||
for h in 2..self.depth { | |||
k0[h] = Block::ZERO; | |||
k1[h] = Block::ZERO; | |||
let sz = 1 << h; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let sz = 1 << h; | |
// How many nodes there are in this layer | |
let sz = 1 << h; |
k: Block, | ||
tree: &mut [Block], | ||
) { | ||
let sz = 1 << depth; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let sz = 1 << depth; | |
// How many nodes there are in this layer | |
let sz = 1 << depth; |
/// * `alpha` - a slice of bits with length `depth`. | ||
/// * `tree` - the destination to write the GGM tree. | ||
pub fn reconstruct(&self, tree: &mut [Block], k: &[Block], alpha: &[bool]) { | ||
let mut pos = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we want some asserts on tree.len()
, k.len()
, alpha.len()
similarly like like in gen()
above ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, we need that asserts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there!
mpz-core/src/aes.rs
Outdated
/// Encrypt block slice. | ||
pub fn encrypt_block_slice(&self, blks: &mut [Block]) { | ||
let len = blks.len(); | ||
let mut buf = [Block::ZERO; AesEncryptor::AES_BLOCK_COUNT]; | ||
for i in 0..len / AesEncryptor::AES_BLOCK_COUNT { | ||
buf.copy_from_slice( | ||
&blks[i * AesEncryptor::AES_BLOCK_COUNT..(i + 1) * AesEncryptor::AES_BLOCK_COUNT], | ||
); | ||
blks[i * AesEncryptor::AES_BLOCK_COUNT..(i + 1) * AesEncryptor::AES_BLOCK_COUNT] | ||
.copy_from_slice(&self.encrypt_many_blocks(buf)); | ||
} | ||
|
||
let remain = len % AesEncryptor::AES_BLOCK_COUNT; | ||
for block in blks[len - remain..].iter_mut() { | ||
*block = self.encrypt_block(*block); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@xiangxiecrypto you should be able to rebase on dev and use the existing impl for this now
Miri CI tests fail on |
#74 should be merged soon |
You can rebase now, CI should be fixed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀
Please do not merge.
This PR mainly adds two functionalities.