test: Improve coverage for protocol and circuit modules (#372)

## Description

Add tests to improve coverage for protocol and circuit modules.

## Tests added

rln/src/circuit/iden3calc.rs:
- test_populate_inputs_missing
- test_populate_inputs_length_mismatch

rln/src/circuit/iden3calc/graph.rs:
- test_div
- test_idiv
- test_fr_mod
- test_u_gte
- test_serde_mont_constant_roundtrip
- test_eval_zero_divisors
- test_eval_pow_and_comparisons
- test_shifts_edges
- test_uno_id_error
- test_evaluate_u256_to_fr_error
- test_u_comparisons_sign
- test_bitwise_ops_basic

rln/src/circuit/iden3calc/storage.rs:
- test_read_message
- test_read_message_variant
- test_write_back_reader
- test_deserialize_inputs
- test_try_from_errors
- test_proto_node_from_constant_panics
- test_read_message_errors
- test_deserialize_invalid_magic
- test_write_back_reader_empty_read_and_flush

rln/src/circuit/qap.rs:
- test_witness_map_from_matrices
- test_h_query_scalars_length

rln/src/circuit/mod.rs:
- test_empty_zkey_and_graph
- test_tree_depth_mismatch

rln/tests/protocol.rs:
- test_witness_serialization_be_roundtrip_and_length_check
- test_proof_values_serialization_be_roundtrip
- test_rln_proof_serialization_be_roundtrip
- test_verify_zk_proof_with_modified_public_value_fails
- test_compute_tree_root_matches_merkle_tree_root
- test_rln_witness_to_bigint_json_fields

## Coverage changed
Before 82.73%
[Download HTML
Report](https://github.com/vacp2p/zerokit/actions/runs/21613783336/artifacts/5351274294)
 
After 88.05%
[Download HTML
Report](https://github.com/vacp2p/zerokit/actions/runs/21655661374/artifacts/5368022860)

## Checklist

- [x] I have run the CI coverage report. Add the `run-coverage` label to
this PR to enable it.
This commit is contained in:
Roman Zajic
2026-02-09 14:01:28 +08:00
committed by GitHub
parent 0846087442
commit ceb6c3ec89
6 changed files with 581 additions and 1 deletions

View File

@@ -107,3 +107,34 @@ fn get_inputs_buffer(size: usize) -> Vec<U256> {
inputs[0] = U256::from(1);
inputs
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
use super::*;
#[test]
fn test_populate_inputs_missing() {
let mut input_list: HashMap<String, Vec<U256>> = HashMap::new();
input_list.insert("missing".to_string(), vec![U256::from(1u64)]);
let input_info: InputSignalsInfo = HashMap::new();
let mut buffer = vec![U256::ZERO; 2];
let err = populate_inputs(&input_list, &input_info, &mut buffer).unwrap_err();
assert!(matches!(err, WitnessCalcError::MissingInput(_)));
}
#[test]
fn test_populate_inputs_length_mismatch() {
let mut input_list: HashMap<String, Vec<U256>> = HashMap::new();
input_list.insert("sig".to_string(), vec![U256::from(1u64)]);
let mut input_info: InputSignalsInfo = HashMap::new();
input_info.insert("sig".to_string(), (0, 2));
let mut buffer = vec![U256::ZERO; 2];
let err = populate_inputs(&input_list, &input_info, &mut buffer).unwrap_err();
assert!(matches!(err, WitnessCalcError::InvalidInputLength { .. }));
}
}

View File

@@ -430,6 +430,7 @@ mod test {
use std::{ops::Div, str::FromStr};
use ruint::uint;
use serde_json;
use super::*;
@@ -566,6 +567,105 @@ mod test {
assert_eq!(result, uint!(1_U256));
}
#[test]
fn test_serde_mont_constant_roundtrip() {
let node = Node::MontConstant(Fr::from(42u64));
let encoded = serde_json::to_vec(&node).unwrap();
let decoded: Node = serde_json::from_slice(&encoded).unwrap();
assert_eq!(node, decoded);
}
#[test]
fn test_eval_zero_divisors() {
let zero = Fr::zero();
let a = Fr::from(7u64);
assert_eq!(Operation::Div.eval_fr(a, zero).unwrap(), Fr::zero());
assert_eq!(Operation::Idiv.eval_fr(a, zero).unwrap(), Fr::zero());
assert_eq!(Operation::Mod.eval_fr(a, zero).unwrap(), Fr::zero());
}
#[test]
fn test_eval_pow_and_comparisons() {
let a = Fr::from(2u64);
let b = Fr::from(5u64);
assert_eq!(Operation::Pow.eval_fr(a, b).unwrap(), Fr::from(32u64));
let a = Fr::from(2u64);
let b = Fr::from(3u64);
assert_eq!(Operation::Eq.eval_fr(a, b).unwrap(), Fr::zero());
assert_eq!(Operation::Neq.eval_fr(a, b).unwrap(), Fr::one());
assert_eq!(Operation::Lt.eval_fr(a, b).unwrap(), Fr::one());
assert_eq!(Operation::Gt.eval_fr(a, b).unwrap(), Fr::zero());
assert_eq!(Operation::Leq.eval_fr(a, b).unwrap(), Fr::one());
assert_eq!(Operation::Geq.eval_fr(a, b).unwrap(), Fr::zero());
let zero = Fr::zero();
let one = Fr::one();
assert_eq!(Operation::Land.eval_fr(zero, one).unwrap(), Fr::zero());
assert_eq!(Operation::Lor.eval_fr(zero, one).unwrap(), Fr::one());
}
#[test]
fn test_shifts_edges() {
let a = Fr::from(5u64);
let b = Fr::zero();
assert_eq!(shl(a, b).unwrap(), a);
assert_eq!(
shl(a, Fr::from(Fr::MODULUS_BIT_SIZE as u64)).unwrap(),
Fr::zero()
);
let b = Fr::zero();
assert_eq!(shr(a, b).unwrap(), a);
assert_eq!(shr(a, Fr::from(254u64)).unwrap(), Fr::zero());
let a = Fr::from(1u64);
assert_eq!(shr(a, Fr::from(64u64)).unwrap(), Fr::zero());
}
#[test]
fn test_uno_id_error() {
let err = UnoOperation::Id.eval_fr(Fr::from(1u64)).unwrap_err();
assert!(err.contains("not implemented"));
}
#[test]
fn test_evaluate_u256_to_fr_error() {
let nodes = vec![Node::Input(0)];
let bad = U256::from_limbs(Fr::MODULUS.0);
let inputs = vec![bad];
let outputs = vec![0usize];
let err = evaluate(&nodes, &inputs, &outputs).unwrap_err();
assert!(err.contains("Failed to convert U256 to Fr"));
}
#[test]
fn test_u_comparisons_sign() {
let pos = uint!(1_U256);
let neg = HALF_M + uint!(1_U256);
let neg2 = HALF_M + uint!(2_U256);
assert_eq!(u_lt(&pos, &neg), uint!(0_U256));
assert_eq!(u_gt(&pos, &neg), uint!(1_U256));
assert_eq!(u_lte(&pos, &neg), uint!(0_U256));
assert_eq!(u_gte(&pos, &neg), uint!(1_U256));
assert_eq!(u_lt(&neg, &pos), uint!(1_U256));
assert_eq!(u_gt(&neg, &pos), uint!(0_U256));
assert_eq!(u_lt(&neg2, &neg), uint!(0_U256));
assert_eq!(u_gt(&neg2, &neg), uint!(1_U256));
}
#[test]
fn test_bitwise_ops_basic() {
let a = Fr::from(5u64);
let b = Fr::from(3u64);
assert_eq!(bit_or(a, b).unwrap(), Fr::from(7u64));
assert_eq!(bit_xor(a, b).unwrap(), Fr::from(6u64));
assert_eq!(bit_and(a, b).unwrap(), Fr::from(1u64));
}
#[test]
fn test_x() {
let x = M.div(uint!(2_U256));

View File

@@ -525,4 +525,87 @@ mod test {
assert_eq!(metadata, metadata_want);
}
#[test]
fn test_try_from_errors() {
let node = proto::Node { node: None };
let err = graph::Node::try_from(node).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
let node = proto::Node {
node: Some(proto::node::Node::Constant(proto::ConstantNode {
value: None,
})),
};
let err = graph::Node::try_from(node).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
let node = proto::Node {
node: Some(proto::node::Node::UnoOp(proto::UnoOpNode {
op: 999,
a_idx: 0,
})),
};
let err = graph::Node::try_from(node).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
let node = proto::Node {
node: Some(proto::node::Node::DuoOp(proto::DuoOpNode {
op: 999,
a_idx: 0,
b_idx: 1,
})),
};
let err = graph::Node::try_from(node).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
let node = proto::Node {
node: Some(proto::node::Node::TresOp(proto::TresOpNode {
op: 999,
a_idx: 0,
b_idx: 1,
c_idx: 2,
})),
};
let err = graph::Node::try_from(node).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}
#[test]
#[should_panic(
expected = "We are not supposed to write Constant to the witnesscalc graph. All Constant should be converted to MontConstant."
)]
fn test_proto_node_from_constant_panics() {
let _ = proto::node::Node::from(&graph::Node::Constant(ruint::aliases::U256::from(1u64)));
}
#[test]
fn test_read_message_errors() {
let mut rw = WriteBackReader::new(std::io::Cursor::new(&[]));
let err = read_message_length(&mut rw).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
let mut buf = Vec::new();
prost::encode_length_delimiter(5, &mut buf).unwrap();
buf.extend_from_slice(&[1u8, 2]);
let mut rw = WriteBackReader::new(std::io::Cursor::new(&buf));
let err = read_message::<_, proto::Node>(&mut rw).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
}
#[test]
fn test_deserialize_invalid_magic() {
let bad = vec![b'x'; WITNESSCALC_GRAPH_MAGIC.len()];
let err = deserialize_witnesscalc_graph(std::io::Cursor::new(&bad)).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}
#[test]
fn test_write_back_reader_empty_read_and_flush() {
let mut rw = WriteBackReader::new(std::io::Cursor::new(&[]));
let mut buf = [];
let n = rw.read(&mut buf).unwrap();
assert_eq!(n, 0);
rw.flush().unwrap();
}
}

View File

@@ -200,3 +200,28 @@ fn read_arkzkey_from_bytes_uncompressed(arkzkey_data: &[u8]) -> Result<Zkey, ZKe
Ok(zkey)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_empty_zkey_and_graph() {
let err = zkey_from_raw(&[]).unwrap_err();
assert!(matches!(err, ZKeyReadError::EmptyBytes));
let err = graph_from_raw(&[], None).err().unwrap();
assert!(matches!(err, GraphReadError::EmptyBytes));
let err = read_arkzkey_from_bytes_uncompressed(&[]).unwrap_err();
assert!(matches!(err, ZKeyReadError::EmptyBytes));
}
#[test]
fn test_tree_depth_mismatch() {
let err = graph_from_raw(GRAPH_BYTES, Some(DEFAULT_TREE_DEPTH + 1))
.err()
.unwrap();
assert!(matches!(err, GraphReadError::TreeDepthMismatch { .. }));
}
}

View File

@@ -117,3 +117,55 @@ impl R1CSToQAP for CircomReduction {
Ok(cfg_into_iter!(scalars).skip(1).step_by(2).collect())
}
}
#[cfg(test)]
mod test {
use ark_ff::{One, Zero};
use ark_poly::GeneralEvaluationDomain;
use ark_relations::r1cs::ConstraintMatrices;
use super::*;
use crate::circuit::Fr;
#[test]
fn test_witness_map_from_matrices() {
let matrices = ConstraintMatrices::<Fr> {
num_instance_variables: 1,
num_witness_variables: 1,
num_constraints: 1,
a_num_non_zero: 1,
b_num_non_zero: 1,
c_num_non_zero: 0,
a: vec![vec![(Fr::one(), 0)]],
b: vec![vec![(Fr::one(), 1)]],
c: vec![vec![]],
};
let full_assignment = vec![Fr::one(), Fr::from(3u64)];
let res = CircomReduction::witness_map_from_matrices::<Fr, GeneralEvaluationDomain<Fr>>(
&matrices,
1,
1,
&full_assignment,
)
.unwrap();
assert_eq!(res.len(), 2);
assert!(res.iter().all(|v| v.is_zero()));
}
#[test]
fn test_h_query_scalars_length() {
let max_power = 2usize;
let domain = GeneralEvaluationDomain::<Fr>::new(2 * max_power + 1).expect("valid domain");
let res = CircomReduction::h_query_scalars::<Fr, GeneralEvaluationDomain<Fr>>(
2,
Fr::from(5u64),
Fr::from(1u64),
Fr::from(3u64),
)
.unwrap();
assert_eq!(res.len(), domain.size() / 2);
}
}

View File

@@ -82,7 +82,7 @@ mod test {
assert!(tree.verify(&rate_commitment, &merkle_proof).unwrap());
}
fn get_test_witness() -> RLNWitnessInput {
fn get_test_witness_and_root() -> (RLNWitnessInput, Fr) {
let leaf_index = 3;
// Generate identity pair
let (identity_secret, id_commitment) = keygen().unwrap();
@@ -98,6 +98,7 @@ mod test {
)
.unwrap();
tree.set(leaf_index, rate_commitment).unwrap();
let root = tree.root();
let merkle_proof = tree.proof(leaf_index).unwrap();
@@ -111,6 +112,57 @@ mod test {
let message_id = Fr::from(1);
RLNWitnessInput::new(
identity_secret,
user_message_limit,
message_id,
merkle_proof.get_path_elements(),
merkle_proof.get_path_index(),
x,
external_nullifier,
)
.map(|witness| (witness, root))
.unwrap()
}
fn get_test_witness() -> RLNWitnessInput {
get_test_witness_and_root().0
}
fn get_test_witness_with_params(
signal: &[u8],
epoch: &[u8],
rln_identifier: &[u8],
message_id: u64,
user_message_limit: u64,
) -> RLNWitnessInput {
let leaf_index = 3;
// Generate identity pair
let (identity_secret, id_commitment) = keygen().unwrap();
let user_message_limit = Fr::from(user_message_limit);
let rate_commitment = poseidon_hash(&[id_commitment, user_message_limit]).unwrap();
//// generate merkle tree
let default_leaf = Fr::from(0);
let mut tree = PoseidonTree::new(
DEFAULT_TREE_DEPTH,
default_leaf,
ConfigOf::<PoseidonTree>::default(),
)
.unwrap();
tree.set(leaf_index, rate_commitment).unwrap();
let merkle_proof = tree.proof(leaf_index).unwrap();
let x = hash_to_field_le(signal).unwrap();
// We set the remaining values to random ones
let epoch = hash_to_field_le(epoch).unwrap();
let rln_identifier = hash_to_field_le(rln_identifier).unwrap();
let external_nullifier = poseidon_hash(&[epoch, rln_identifier]).unwrap();
let message_id = Fr::from(message_id);
RLNWitnessInput::new(
identity_secret,
user_message_limit,
@@ -290,4 +342,241 @@ mod test {
assert_eq!(identity_secret, expected_identity_secret_seed_phrase);
assert_eq!(id_commitment, expected_id_commitment_seed_phrase);
}
#[test]
fn test_extended_keygen_relations() {
let (trapdoor, nullifier, identity_secret, id_commitment) = extended_keygen().unwrap();
let expected_identity_secret = poseidon_hash(&[trapdoor, nullifier]).unwrap();
let expected_id_commitment = poseidon_hash(&[identity_secret]).unwrap();
assert_eq!(identity_secret, expected_identity_secret);
assert_eq!(id_commitment, expected_id_commitment);
}
#[test]
fn test_extended_seeded_keygen_determinism() {
let seed = b"test-seed-extended";
let first = extended_seeded_keygen(seed).unwrap();
let second = extended_seeded_keygen(seed).unwrap();
assert_eq!(first, second);
let (trapdoor, nullifier, identity_secret, id_commitment) = first;
let expected_identity_secret = poseidon_hash(&[trapdoor, nullifier]).unwrap();
let expected_id_commitment = poseidon_hash(&[identity_secret]).unwrap();
assert_eq!(identity_secret, expected_identity_secret);
assert_eq!(id_commitment, expected_id_commitment);
}
#[test]
fn test_witness_serialization_be_roundtrip_and_length_check() {
// Test with default witness
let witness = get_test_witness();
let ser = rln_witness_to_bytes_be(&witness).unwrap();
let (deser, _) = bytes_be_to_rln_witness(&ser).unwrap();
assert_eq!(witness, deser);
// Test with varied witness
let witness2 = get_test_witness_with_params(
b"different signal",
b"another epoch",
b"alt rln id",
42,
200,
);
let ser2 = rln_witness_to_bytes_be(&witness2).unwrap();
let (deser2, _) = bytes_be_to_rln_witness(&ser2).unwrap();
assert_eq!(witness2, deser2);
// Test with extreme values (large message_id and limit)
let witness3 = get_test_witness_with_params(
b"extreme signal",
b"extreme epoch",
b"extreme id",
1000000,
2000000,
);
let ser3 = rln_witness_to_bytes_be(&witness3).unwrap();
let (deser3, _) = bytes_be_to_rln_witness(&ser3).unwrap();
assert_eq!(witness3, deser3);
let mut bad = ser.clone();
bad.push(0);
assert!(matches!(
bytes_be_to_rln_witness(&bad),
Err(ProtocolError::InvalidReadLen(_, _))
));
}
#[test]
fn test_proof_values_serialization_be_roundtrip() {
// Test with default witness
let witness = get_test_witness();
let proof_values = proof_values_from_witness(&witness).unwrap();
let ser = rln_proof_values_to_bytes_be(&proof_values);
let (deser, _) = bytes_be_to_rln_proof_values(&ser).unwrap();
assert_eq!(proof_values, deser);
// Test with varied witness
let witness2 = get_test_witness_with_params(b"another signal", b"epoch2", b"id2", 10, 150);
let proof_values2 = proof_values_from_witness(&witness2).unwrap();
let ser2 = rln_proof_values_to_bytes_be(&proof_values2);
let (deser2, _) = bytes_be_to_rln_proof_values(&ser2).unwrap();
assert_eq!(proof_values2, deser2);
}
#[test]
fn test_rln_proof_serialization_be_roundtrip() {
let witness = get_test_witness();
let proving_key = zkey_from_folder();
let graph_data = graph_from_folder();
let proof = generate_zk_proof(proving_key, &witness, graph_data).unwrap();
let proof_values = proof_values_from_witness(&witness).unwrap();
let rln_proof = RLNProof {
proof: proof.clone(),
proof_values,
};
let ser = rln_proof_to_bytes_be(&rln_proof).unwrap();
let (deser, _) = bytes_be_to_rln_proof(&ser).unwrap();
assert_eq!(rln_proof.proof, deser.proof);
assert_eq!(rln_proof.proof_values, deser.proof_values);
}
#[test]
fn test_verify_zk_proof_with_modified_public_value_fails() {
let witness = get_test_witness();
let proving_key = zkey_from_folder();
let graph_data = graph_from_folder();
let proof = generate_zk_proof(proving_key, &witness, graph_data).unwrap();
let mut proof_values = proof_values_from_witness(&witness).unwrap();
proof_values.root += Fr::from(1u64);
let verified = verify_zk_proof(&proving_key.0.vk, &proof, &proof_values).unwrap();
assert!(!verified);
}
#[test]
fn test_compute_tree_root_matches_merkle_tree_root() {
// Test with default witness
let (witness, root) = get_test_witness_and_root();
let computed_root = compute_tree_root(
witness.identity_secret(),
witness.user_message_limit(),
witness.path_elements(),
witness.identity_path_index(),
)
.unwrap();
assert_eq!(computed_root, root);
// Test with varied witness
let witness2 =
get_test_witness_with_params(b"root test signal", b"root epoch", b"root id", 25, 300);
let leaf_index = 3;
let id_commitment = poseidon_hash(&[**witness2.identity_secret()]).unwrap();
let rate_commitment =
poseidon_hash(&[id_commitment, *witness2.user_message_limit()]).unwrap();
let default_leaf = Fr::from(0);
let mut tree = PoseidonTree::new(
DEFAULT_TREE_DEPTH,
default_leaf,
ConfigOf::<PoseidonTree>::default(),
)
.unwrap();
tree.set(leaf_index, rate_commitment).unwrap();
let root2 = tree.root();
let computed_root2 = compute_tree_root(
witness2.identity_secret(),
witness2.user_message_limit(),
witness2.path_elements(),
witness2.identity_path_index(),
)
.unwrap();
assert_eq!(computed_root2, root2);
}
#[test]
fn test_rln_witness_to_bigint_json_fields() {
// Test with default witness
let witness = get_test_witness();
let json = rln_witness_to_bigint_json(&witness).unwrap();
assert_eq!(
json["identitySecret"].as_str().unwrap(),
to_bigint(witness.identity_secret()).to_str_radix(10)
);
assert_eq!(
json["userMessageLimit"].as_str().unwrap(),
to_bigint(witness.user_message_limit()).to_str_radix(10)
);
assert_eq!(
json["messageId"].as_str().unwrap(),
to_bigint(witness.message_id()).to_str_radix(10)
);
assert_eq!(
json["x"].as_str().unwrap(),
to_bigint(witness.x()).to_str_radix(10)
);
assert_eq!(
json["externalNullifier"].as_str().unwrap(),
to_bigint(witness.external_nullifier()).to_str_radix(10)
);
assert_eq!(
json["pathElements"].as_array().unwrap().len(),
witness.path_elements().len()
);
assert_eq!(
json["identityPathIndex"].as_array().unwrap().len(),
witness.identity_path_index().len()
);
// Test with varied witness
let witness2 =
get_test_witness_with_params(b"json test signal", b"json epoch", b"json id", 99, 500);
let json2 = rln_witness_to_bigint_json(&witness2).unwrap();
assert_eq!(
json2["identitySecret"].as_str().unwrap(),
to_bigint(witness2.identity_secret()).to_str_radix(10)
);
assert_eq!(
json2["userMessageLimit"].as_str().unwrap(),
to_bigint(witness2.user_message_limit()).to_str_radix(10)
);
assert_eq!(
json2["messageId"].as_str().unwrap(),
to_bigint(witness2.message_id()).to_str_radix(10)
);
assert_eq!(
json2["x"].as_str().unwrap(),
to_bigint(witness2.x()).to_str_radix(10)
);
assert_eq!(
json2["externalNullifier"].as_str().unwrap(),
to_bigint(witness2.external_nullifier()).to_str_radix(10)
);
assert_eq!(
json2["pathElements"].as_array().unwrap().len(),
witness2.path_elements().len()
);
assert_eq!(
json2["identityPathIndex"].as_array().unwrap().len(),
witness2.identity_path_index().len()
);
}
}