From 60e3369621b060a64542cd692306a2cc3ce2079e Mon Sep 17 00:00:00 2001 From: G <28568419+s1fr0@users.noreply.github.com> Date: Mon, 28 Nov 2022 12:05:33 +0100 Subject: [PATCH] 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 --- rln/src/ffi.rs | 195 +++++++++++++++++++++++++++++++++++++++++- rln/src/protocol.rs | 31 ++++++- rln/src/public.rs | 201 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 419 insertions(+), 8 deletions(-) diff --git a/rln/src/ffi.rs b/rln/src/ffi.rs index 3aaa727..fc1e90e 100644 --- a/rln/src/ffi.rs +++ b/rln/src/ffi.rs @@ -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 = 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::::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::::uninit(); @@ -931,7 +956,7 @@ mod test { let input_buffer = &Buffer::from(serialized.as_ref()); let mut output_buffer = MaybeUninit::::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::::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::::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 ] + let mut serialized1: Vec = 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::::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::::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::::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::::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 ] + // Note that epoch is the same as before + let mut serialized: Vec = 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::::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::::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() { diff --git a/rln/src/protocol.rs b/rln/src/protocol.rs index d480e8d..ed58cfd 100644 --- a/rln/src/protocol.rs +++ b/rln/src/protocol.rs @@ -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 { 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 { + // 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 /////////////////////////////////////////////////////// diff --git a/rln/src/public.rs b/rln/src/public.rs index aec0da3..8e76ba5 100644 --- a/rln/src/public.rs +++ b/rln/src/public.rs @@ -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 ]` (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::::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( + &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 = 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 = 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 ] + let mut serialized1: Vec = 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::::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::::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::::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 ] + let mut serialized3: Vec = 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::::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::::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()); + } }