feat(rln): add recover identity secret API (#80)

* feat(rln): add API to recover id_secret when a user is slashed

* feat(rln): add RLN API for recovering id_secret

* feat(rln): add recover_id_secret FFI API; update public API docs

* fix(rln): address reviewers' comments

* fix(rln): removing ; for clippy
This commit is contained in:
G 2022-11-28 12:05:33 +01:00 committed by GitHub
parent 284e51483c
commit 60e3369621
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 419 additions and 8 deletions

View File

@ -297,6 +297,31 @@ pub extern "C" fn seeded_key_gen(
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
#[no_mangle]
pub extern "C" fn recover_id_secret(
ctx: *const RLN,
input_proof_buffer_1: *const Buffer,
input_proof_buffer_2: *const Buffer,
output_buffer: *mut Buffer,
) -> bool {
let rln = unsafe { &*ctx };
let input_proof_data_1 = <&[u8]>::from(unsafe { &*input_proof_buffer_1 });
let input_proof_data_2 = <&[u8]>::from(unsafe { &*input_proof_buffer_2 });
let mut output_data: Vec<u8> = Vec::new();
if rln
.recover_id_secret(input_proof_data_1, input_proof_data_2, &mut output_data)
.is_ok()
{
unsafe { *output_buffer = Buffer::from(&output_data[..]) };
std::mem::forget(output_data);
true
} else {
std::mem::forget(output_data);
false
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
#[no_mangle]
pub extern "C" fn hash(
@ -400,7 +425,7 @@ mod test {
let leaves_ser = vec_fr_to_bytes_le(&leaves);
let input_buffer = &Buffer::from(leaves_ser.as_ref());
let success = init_tree_with_leaves(rln_pointer, input_buffer);
assert!(success, "set leaves call failed");
assert!(success, "init tree with leaves call failed");
// We get the root of the tree obtained adding leaves in batch
let mut output_buffer = MaybeUninit::<Buffer>::uninit();
@ -891,7 +916,7 @@ mod test {
let leaves_ser = vec_fr_to_bytes_le(&leaves);
let input_buffer = &Buffer::from(leaves_ser.as_ref());
let success = init_tree_with_leaves(rln_pointer, input_buffer);
assert!(success, "set leaves call failed");
assert!(success, "init tree with leaves call failed");
// We generate a new identity pair
let mut output_buffer = MaybeUninit::<Buffer>::uninit();
@ -931,7 +956,7 @@ mod test {
let input_buffer = &Buffer::from(serialized.as_ref());
let mut output_buffer = MaybeUninit::<Buffer>::uninit();
let success = generate_rln_proof(rln_pointer, input_buffer, output_buffer.as_mut_ptr());
assert!(success, "set leaves call failed");
assert!(success, "generate rln proof call failed");
let output_buffer = unsafe { output_buffer.assume_init() };
// result_data is [ proof<128> | share_y<32> | nullifier<32> | root<32> | epoch<32> | share_x<32> | rln_identifier<32> ]
let mut proof_data = <&[u8]>::from(&output_buffer).to_vec();
@ -1016,7 +1041,7 @@ mod test {
let input_buffer = &Buffer::from(serialized.as_ref());
let mut output_buffer = MaybeUninit::<Buffer>::uninit();
let success = generate_rln_proof(rln_pointer, input_buffer, output_buffer.as_mut_ptr());
assert!(success, "set leaves call failed");
assert!(success, "generate rln proof call failed");
let output_buffer = unsafe { output_buffer.assume_init() };
// result_data is [ proof<128> | share_y<32> | nullifier<32> | root<32> | epoch<32> | share_x<32> | rln_identifier<32> ]
let mut proof_data = <&[u8]>::from(&output_buffer).to_vec();
@ -1079,6 +1104,168 @@ mod test {
assert_eq!(proof_is_valid, true);
}
#[test]
// Computes and verifies an RLN ZK proof using FFI APIs
fn test_recover_id_secret_ffi() {
let tree_height = TEST_TREE_HEIGHT;
// We create a RLN instance
let mut rln_pointer = MaybeUninit::<*mut RLN>::uninit();
let input_buffer = &Buffer::from(TEST_RESOURCES_FOLDER.as_bytes());
let success = new(tree_height, input_buffer, rln_pointer.as_mut_ptr());
assert!(success, "RLN object creation failed");
let rln_pointer = unsafe { &mut *rln_pointer.assume_init() };
// We generate a new identity pair
let mut output_buffer = MaybeUninit::<Buffer>::uninit();
let success = key_gen(rln_pointer, output_buffer.as_mut_ptr());
assert!(success, "key gen call failed");
let output_buffer = unsafe { output_buffer.assume_init() };
let result_data = <&[u8]>::from(&output_buffer).to_vec();
let (identity_secret, read) = bytes_le_to_fr(&result_data);
let (id_commitment, _) = bytes_le_to_fr(&result_data[read..].to_vec());
// We set as leaf id_commitment, its index would be equal to 0 since tree is empty
let leaf_ser = fr_to_bytes_le(&id_commitment);
let input_buffer = &Buffer::from(leaf_ser.as_ref());
let success = set_next_leaf(rln_pointer, input_buffer);
assert!(success, "set next leaf call failed");
let identity_index: u64 = 0;
// We generate two proofs using same epoch but different signals.
// We generate two random signals
let mut rng = rand::thread_rng();
let signal1: [u8; 32] = rng.gen();
let signal1_len = u64::try_from(signal1.len()).unwrap();
// We generate two random signals
let signal2: [u8; 32] = rng.gen();
let signal2_len = u64::try_from(signal2.len()).unwrap();
// We generate a random epoch
let epoch = hash_to_field(b"test-epoch");
// We prepare input for generate_rln_proof API
// input_data is [ id_key<32> | id_index<8> | epoch<32> | signal_len<8> | signal<var> ]
let mut serialized1: Vec<u8> = Vec::new();
serialized1.append(&mut fr_to_bytes_le(&identity_secret));
serialized1.append(&mut identity_index.to_le_bytes().to_vec());
serialized1.append(&mut fr_to_bytes_le(&epoch));
// The first part is the same for both proof input, so we clone
let mut serialized2 = serialized1.clone();
// We attach the first signal to the first proof input
serialized1.append(&mut signal1_len.to_le_bytes().to_vec());
serialized1.append(&mut signal1.to_vec());
// We attach the second signal to the first proof input
serialized2.append(&mut signal2_len.to_le_bytes().to_vec());
serialized2.append(&mut signal2.to_vec());
// We call generate_rln_proof for first proof values
let input_buffer = &Buffer::from(serialized1.as_ref());
let mut output_buffer = MaybeUninit::<Buffer>::uninit();
let success = generate_rln_proof(rln_pointer, input_buffer, output_buffer.as_mut_ptr());
assert!(success, "generate rln proof call failed");
let output_buffer = unsafe { output_buffer.assume_init() };
// result_data is [ proof<128> | share_y<32> | nullifier<32> | root<32> | epoch<32> | share_x<32> | rln_identifier<32> ]
let proof_data_1 = <&[u8]>::from(&output_buffer).to_vec();
// We call generate_rln_proof
let input_buffer = &Buffer::from(serialized2.as_ref());
let mut output_buffer = MaybeUninit::<Buffer>::uninit();
let success = generate_rln_proof(rln_pointer, input_buffer, output_buffer.as_mut_ptr());
assert!(success, "generate rln proof call failed");
let output_buffer = unsafe { output_buffer.assume_init() };
// result_data is [ proof<128> | share_y<32> | nullifier<32> | root<32> | epoch<32> | share_x<32> | rln_identifier<32> ]
let proof_data_2 = <&[u8]>::from(&output_buffer).to_vec();
let input_proof_buffer_1 = &Buffer::from(proof_data_1.as_ref());
let input_proof_buffer_2 = &Buffer::from(proof_data_2.as_ref());
let mut output_buffer = MaybeUninit::<Buffer>::uninit();
let success = recover_id_secret(
rln_pointer,
input_proof_buffer_1,
input_proof_buffer_2,
output_buffer.as_mut_ptr(),
);
assert!(success, "recover id secret call failed");
let output_buffer = unsafe { output_buffer.assume_init() };
let serialized_id_secret = <&[u8]>::from(&output_buffer).to_vec();
// We passed two shares for the same secret, so recovery should be successful
// To check it, we ensure that recovered_id_secret is non-empty
assert!(!serialized_id_secret.is_empty());
// We check if the recovered id secret corresponds to the original one
let (recovered_id_secret, _) = bytes_le_to_fr(&serialized_id_secret);
assert_eq!(recovered_id_secret, identity_secret);
// We now test that computing_id_secret is unsuccessful if shares computed from two different id secrets but within same epoch are passed
// We generate a new identity pair
let mut output_buffer = MaybeUninit::<Buffer>::uninit();
let success = key_gen(rln_pointer, output_buffer.as_mut_ptr());
assert!(success, "key gen call failed");
let output_buffer = unsafe { output_buffer.assume_init() };
let result_data = <&[u8]>::from(&output_buffer).to_vec();
let (identity_secret_new, read) = bytes_le_to_fr(&result_data);
let (id_commitment_new, _) = bytes_le_to_fr(&result_data[read..].to_vec());
// We set as leaf id_commitment, its index would be equal to 1 since at 0 there is id_commitment
let leaf_ser = fr_to_bytes_le(&id_commitment_new);
let input_buffer = &Buffer::from(leaf_ser.as_ref());
let success = set_next_leaf(rln_pointer, input_buffer);
assert!(success, "set next leaf call failed");
let identity_index_new: u64 = 1;
// We generate a random signals
let signal3: [u8; 32] = rng.gen();
let signal3_len = u64::try_from(signal3.len()).unwrap();
// We prepare input for generate_rln_proof API
// input_data is [ id_key<32> | id_index<8> | epoch<32> | signal_len<8> | signal<var> ]
// Note that epoch is the same as before
let mut serialized: Vec<u8> = Vec::new();
serialized.append(&mut fr_to_bytes_le(&identity_secret_new));
serialized.append(&mut identity_index_new.to_le_bytes().to_vec());
serialized.append(&mut fr_to_bytes_le(&epoch));
serialized.append(&mut signal3_len.to_le_bytes().to_vec());
serialized.append(&mut signal3.to_vec());
// We call generate_rln_proof
let input_buffer = &Buffer::from(serialized.as_ref());
let mut output_buffer = MaybeUninit::<Buffer>::uninit();
let success = generate_rln_proof(rln_pointer, input_buffer, output_buffer.as_mut_ptr());
assert!(success, "generate rln proof call failed");
let output_buffer = unsafe { output_buffer.assume_init() };
// result_data is [ proof<128> | share_y<32> | nullifier<32> | root<32> | epoch<32> | share_x<32> | rln_identifier<32> ]
let proof_data_3 = <&[u8]>::from(&output_buffer).to_vec();
// We attempt to recover the secret using share1 (coming from identity_secret) and share3 (coming from identity_secret_new)
let input_proof_buffer_1 = &Buffer::from(proof_data_1.as_ref());
let input_proof_buffer_3 = &Buffer::from(proof_data_3.as_ref());
let mut output_buffer = MaybeUninit::<Buffer>::uninit();
let success = recover_id_secret(
rln_pointer,
input_proof_buffer_1,
input_proof_buffer_3,
output_buffer.as_mut_ptr(),
);
assert!(success, "recover id secret call failed");
let output_buffer = unsafe { output_buffer.assume_init() };
let serialized_id_secret = <&[u8]>::from(&output_buffer).to_vec();
// We passed two shares for different secrets, so recovery should be not successful
// To check it, we ensure that recovered_id_secret is empty
assert!(serialized_id_secret.is_empty());
}
#[test]
// Tests hash to field using FFI APIs
fn test_seeded_keygen_ffi() {

View File

@ -253,8 +253,7 @@ pub fn proof_values_from_witness(rln_witness: &RLNWitnessInput) -> RLNProofValue
// y share
let a_0 = rln_witness.identity_secret;
let a_1 = poseidon_hash(&[a_0, rln_witness.epoch]);
let y = rln_witness.x * a_1;
let y = y + a_0;
let y = a_0 + rln_witness.x * a_1;
// Nullifier
let nullifier = poseidon_hash(&[a_1, rln_witness.rln_identifier]);
@ -290,6 +289,8 @@ pub fn serialize_proof_values(rln_proof_values: &RLNProofValues) -> Vec<u8> {
serialized
}
// Note: don't forget to skip the 128 bytes ZK proof, if serialized contains it.
// This proc deserialzies only proof _values_, i.e. circuit outputs, not the zk proof.
pub fn deserialize_proof_values(serialized: &[u8]) -> (RLNProofValues, usize) {
let mut all_read: usize = 0;
@ -382,7 +383,7 @@ pub fn compute_tree_root(
}
///////////////////////////////////////////////////////
// Signal/nullifier utility functions
// Protocol utility functions
///////////////////////////////////////////////////////
// Generates a tupe (identity_secret, id_commitment) where
@ -426,6 +427,30 @@ pub fn hash_to_field(signal: &[u8]) -> Fr {
el
}
pub fn compute_id_secret(share1: (Fr, Fr), share2: (Fr, Fr), epoch: Fr) -> Result<Fr, String> {
// Assuming a0 is the identity secret and a1 = poseidonHash([a0, epoch]),
// a (x,y) share satisfies the following relation
// y = a_0 + x * a_1
let (x1, y1) = share1;
let (x2, y2) = share2;
// If the two input shares were computed for the same epoch and identity secret, we can recover the latter
// y1 = a_0 + x1 * a_1
// y2 = a_0 + x2 * a_1
let a_1 = (y1 - y2) / (x1 - x2);
let a_0 = y1 - x1 * a_1;
// If shares come from the same polynomial, a0 is correctly recovered and a1 = poseidonHash([a0, epoch])
let computed_a_1 = poseidon_hash(&[a_0, epoch]);
if a_1 == computed_a_1 {
// We successfully recovered the identity secret
return Ok(a_0);
} else {
return Err("Cannot recover id_secret from provided shares".into());
}
}
///////////////////////////////////////////////////////
// zkSNARK utility functions
///////////////////////////////////////////////////////

View File

@ -528,7 +528,7 @@ impl RLN<'_> {
/// rln.generate_rln_proof(&mut input_buffer, &mut output_buffer)
/// .unwrap();
///
/// // proof_data is [ proof<128> | share_y<32> | nullifier<32> | root<32> | epoch<32> | share_x<32> | rln_identifier<32> ]
/// // proof_data is [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> ]
/// let mut proof_data = output_buffer.into_inner();
/// ```
#[cfg(not(target_arch = "wasm32"))]
@ -830,6 +830,77 @@ impl RLN<'_> {
Ok(())
}
/// Recovers the identity secret from two set of proof values computed for same secret in same epoch.
///
/// Input values are:
/// - `input_proof_data_1`: a reader for the serialization of a RLN zkSNARK proof concatenated with a serialization of the circuit output values and -optionally- the signal information, i.e. either `[ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> ]` or `[ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> | signal_len<8> | signal<var> ]` (to maintain compatibility with both output of [`generate_rln_proof`](crate::public::RLN::generate_rln_proof) and input of [`verify_rln_proof`](crate::public::RLN::verify_rln_proof))
/// - `input_proof_data_2`: same as `input_proof_data_1`
///
/// Output values are:
/// - `output_data`: a writer receiving the serialization of the recovered identity secret field element if correctly recovered (serialization done with [`rln::utils::fr_to_bytes_le`](crate::utils::fr_to_bytes_le)), a writer receiving an empty byte vector if not.
///
/// Example
/// ```
/// // identity_secret, proof_data_1 and proof_data_2 are computed as in the example code snippet provided for rln::public::RLN::generate_rln_proof using same identity secret and epoch (but not necessarily same signal)
///
/// let mut input_proof_data_1 = Cursor::new(proof_data_1);
/// let mut input_proof_data_2 = Cursor::new(proof_data_2);
/// let mut output_buffer = Cursor::new(Vec::<u8>::new());
/// rln.recover_id_secret(
/// &mut input_proof_data_1,
/// &mut input_proof_data_2,
/// &mut output_buffer,
/// )
/// .unwrap();
///
/// let serialized_id_secret = output_buffer.into_inner();
///
/// // We ensure that a non-empty value is written to output_buffer
/// assert!(!serialized_id_secret.is_empty());
///
/// // We check if the recovered id secret corresponds to the original one
/// let (recovered_id_secret, _) = bytes_le_to_fr(&serialized_id_secret);
/// assert_eq!(recovered_id_secret, identity_secret);
/// ```
pub fn recover_id_secret<R: Read, W: Write>(
&self,
mut input_proof_data_1: R,
mut input_proof_data_2: R,
mut output_data: W,
) -> io::Result<()> {
// We deserialize the two proofs and we get the corresponding RLNProofValues objects
let mut serialized: Vec<u8> = Vec::new();
input_proof_data_1.read_to_end(&mut serialized)?;
// We skip deserialization of the zk-proof at the beginning
let (proof_values_1, _) = deserialize_proof_values(&serialized[128..].to_vec());
let mut serialized: Vec<u8> = Vec::new();
input_proof_data_2.read_to_end(&mut serialized)?;
// We skip deserialization of the zk-proof at the beginning
let (proof_values_2, _) = deserialize_proof_values(&serialized[128..].to_vec());
// We continue only if the proof values are for the same epoch
// The idea is that proof values that go as input to this function are verified first (with zk-proof verify), hence ensuring validity of epoch and other fields.
// Only in case all fields are valid, an external_nullifier for the message will be stored (otherwise signal/proof will be simply discarded)
// If the nullifier matches one already seen, we can recovery of identity secret.
if proof_values_1.epoch == proof_values_2.epoch {
// We extract the two shares
let share1 = (proof_values_1.x, proof_values_1.y);
let share2 = (proof_values_2.x, proof_values_2.y);
// We recover the secret
let recovered_id_secret = compute_id_secret(share1, share2, proof_values_1.epoch);
// If an id secret is recovered, we write it to output_data, otherwise nothing will be written.
if recovered_id_secret.is_ok() {
let id_secret = recovered_id_secret.unwrap();
output_data.write_all(&fr_to_bytes_le(&id_secret))?;
}
}
Ok(())
}
/// Hashes an input signal to an element in the working prime field.
///
/// The result is computed as the Keccak256 of the input signal modulo the prime field characteristic.
@ -1619,4 +1690,132 @@ mod test {
assert!(verified);
}
#[test]
fn test_recover_id_secret() {
let tree_height = TEST_TREE_HEIGHT;
// We create a new RLN instance
let input_buffer = Cursor::new(TEST_RESOURCES_FOLDER);
let mut rln = RLN::new(tree_height, input_buffer);
// Generate identity pair
let (identity_secret, id_commitment) = keygen();
// We set as leaf id_commitment after storing its index
let identity_index = u64::try_from(rln.tree.leaves_set()).unwrap();
let mut buffer = Cursor::new(fr_to_bytes_le(&id_commitment));
rln.set_next_leaf(&mut buffer).unwrap();
// We generate two random signals
let mut rng = rand::thread_rng();
let signal1: [u8; 32] = rng.gen();
let signal1_len = u64::try_from(signal1.len()).unwrap();
let signal2: [u8; 32] = rng.gen();
let signal2_len = u64::try_from(signal2.len()).unwrap();
// We generate a random epoch
let epoch = hash_to_field(b"test-epoch");
// We generate two proofs using same epoch but different signals.
// We prepare input for generate_rln_proof API
// input_data is [ id_key<32> | id_index<8> | epoch<32> | signal_len<8> | signal<var> ]
let mut serialized1: Vec<u8> = Vec::new();
serialized1.append(&mut fr_to_bytes_le(&identity_secret));
serialized1.append(&mut identity_index.to_le_bytes().to_vec());
serialized1.append(&mut fr_to_bytes_le(&epoch));
// The first part is the same for both proof input, so we clone
let mut serialized2 = serialized1.clone();
// We attach the first signal to the first proof input
serialized1.append(&mut signal1_len.to_le_bytes().to_vec());
serialized1.append(&mut signal1.to_vec());
// We attach the second signal to the first proof input
serialized2.append(&mut signal2_len.to_le_bytes().to_vec());
serialized2.append(&mut signal2.to_vec());
// We generate the first proof
let mut input_buffer = Cursor::new(serialized1);
let mut output_buffer = Cursor::new(Vec::<u8>::new());
rln.generate_rln_proof(&mut input_buffer, &mut output_buffer)
.unwrap();
let proof_data_1 = output_buffer.into_inner();
// We generate the second proof
let mut input_buffer = Cursor::new(serialized2);
let mut output_buffer = Cursor::new(Vec::<u8>::new());
rln.generate_rln_proof(&mut input_buffer, &mut output_buffer)
.unwrap();
let proof_data_2 = output_buffer.into_inner();
let mut input_proof_data_1 = Cursor::new(proof_data_1.clone());
let mut input_proof_data_2 = Cursor::new(proof_data_2);
let mut output_buffer = Cursor::new(Vec::<u8>::new());
rln.recover_id_secret(
&mut input_proof_data_1,
&mut input_proof_data_2,
&mut output_buffer,
)
.unwrap();
let serialized_id_secret = output_buffer.into_inner();
// We ensure that a non-empty value is written to output_buffer
assert!(!serialized_id_secret.is_empty());
// We check if the recovered id secret corresponds to the original one
let (recovered_id_secret, _) = bytes_le_to_fr(&serialized_id_secret);
assert_eq!(recovered_id_secret, identity_secret);
// We now test that computing_id_secret is unsuccessful if shares computed from two different id secrets but within same epoch are passed
// We generate a new identity pair
let (identity_secret_new, id_commitment_new) = keygen();
// We add it to the tree
let identity_index_new = u64::try_from(rln.tree.leaves_set()).unwrap();
let mut buffer = Cursor::new(fr_to_bytes_le(&id_commitment_new));
rln.set_next_leaf(&mut buffer).unwrap();
// We generate a random signals
let signal3: [u8; 32] = rng.gen();
let signal3_len = u64::try_from(signal3.len()).unwrap();
// We prepare proof input. Note that epoch is the same as before
// input_data is [ id_key<32> | id_index<8> | epoch<32> | signal_len<8> | signal<var> ]
let mut serialized3: Vec<u8> = Vec::new();
serialized3.append(&mut fr_to_bytes_le(&identity_secret_new));
serialized3.append(&mut identity_index_new.to_le_bytes().to_vec());
serialized3.append(&mut fr_to_bytes_le(&epoch));
serialized3.append(&mut signal3_len.to_le_bytes().to_vec());
serialized3.append(&mut signal3.to_vec());
// We generate the proof
let mut input_buffer = Cursor::new(serialized3);
let mut output_buffer = Cursor::new(Vec::<u8>::new());
rln.generate_rln_proof(&mut input_buffer, &mut output_buffer)
.unwrap();
let proof_data_3 = output_buffer.into_inner();
// We attempt to recover the secret using share1 (coming from identity_secret) and share3 (coming from identity_secret_new)
let mut input_proof_data_1 = Cursor::new(proof_data_1.clone());
let mut input_proof_data_3 = Cursor::new(proof_data_3);
let mut output_buffer = Cursor::new(Vec::<u8>::new());
rln.recover_id_secret(
&mut input_proof_data_1,
&mut input_proof_data_3,
&mut output_buffer,
)
.unwrap();
let serialized_id_secret = output_buffer.into_inner();
// We ensure that an empty value was written to output_buffer, i.e. no secret is recovered
assert!(serialized_id_secret.is_empty());
}
}