use crate::error::WalletKitError;
use alloy_core::sol_types::SolValue;
use ruint_uniffi::Uint256;
#[cfg(feature = "semaphore")]
use semaphore_rs::protocol::{generate_nullifier_hash, generate_proof};
use semaphore_rs::{
hash_to_field, identity, packed_proof::PackedProof, protocol::Proof, MODULUS,
};
use serde::Serialize;
use super::{credential_type::CredentialType, merkle_tree::MerkleTreeProof};
#[derive(Clone, PartialEq, Eq, Debug, uniffi::Object)]
pub struct ProofContext {
pub external_nullifier: Uint256,
pub credential_type: CredentialType,
pub signal_hash: Uint256,
pub require_mined_proof: bool,
}
#[uniffi::export]
impl ProofContext {
#[must_use]
#[uniffi::constructor]
pub fn new(
app_id: &str,
action: Option<String>,
signal: Option<String>,
credential_type: CredentialType,
) -> Self {
Self::new_from_bytes(
app_id,
action.map(std::string::String::into_bytes),
signal.map(std::string::String::into_bytes),
credential_type,
)
}
#[must_use]
#[uniffi::constructor]
#[allow(clippy::needless_pass_by_value)]
pub fn new_from_bytes(
app_id: &str,
action: Option<Vec<u8>>,
signal: Option<Vec<u8>>,
credential_type: CredentialType,
) -> Self {
let signal_hash =
Uint256::from(hash_to_field(signal.unwrap_or_default().as_slice()));
Self::new_from_signal_hash_unchecked(
app_id,
action,
credential_type,
&signal_hash,
)
}
#[uniffi::constructor]
pub fn new_from_signal_hash(
app_id: &str,
action: Option<Vec<u8>>,
credential_type: CredentialType,
signal_hash: &Uint256,
) -> Result<Self, WalletKitError> {
if signal_hash.0 >= MODULUS {
return Err(WalletKitError::InvalidNumber);
}
Ok(Self::new_from_signal_hash_unchecked(
app_id,
action,
credential_type,
signal_hash,
))
}
#[must_use]
pub const fn get_external_nullifier(&self) -> Uint256 {
self.external_nullifier
}
#[must_use]
pub const fn get_signal_hash(&self) -> Uint256 {
self.signal_hash
}
#[must_use]
pub const fn get_credential_type(&self) -> CredentialType {
self.credential_type
}
}
impl ProofContext {
fn new_from_signal_hash_unchecked(
app_id: &str,
action: Option<Vec<u8>>,
credential_type: CredentialType,
signal_hash: &Uint256,
) -> Self {
let mut pre_image = hash_to_field(app_id.as_bytes()).abi_encode_packed();
if let Some(action) = action {
pre_image.extend_from_slice(&action);
}
let external_nullifier = hash_to_field(&pre_image).into();
Self {
external_nullifier,
credential_type,
signal_hash: *signal_hash,
require_mined_proof: false,
}
}
}
#[uniffi::export]
#[cfg(feature = "legacy-nullifiers")]
impl ProofContext {
#[must_use]
#[uniffi::constructor]
pub fn legacy_new_from_pre_image_external_nullifier(
external_nullifier: &[u8],
credential_type: CredentialType,
signal: Option<Vec<u8>>,
require_mined_proof: bool,
) -> Self {
let external_nullifier: Uint256 = hash_to_field(external_nullifier).into();
Self {
external_nullifier,
credential_type,
signal_hash: hash_to_field(signal.unwrap_or_default().as_slice()).into(),
require_mined_proof,
}
}
#[uniffi::constructor]
pub fn legacy_new_from_raw_external_nullifier(
external_nullifier: &Uint256,
credential_type: CredentialType,
signal: Option<Vec<u8>>,
require_mined_proof: bool,
) -> Result<Self, WalletKitError> {
if external_nullifier.0 >= MODULUS {
return Err(WalletKitError::InvalidNumber);
}
Ok(Self {
external_nullifier: *external_nullifier,
credential_type,
signal_hash: hash_to_field(signal.unwrap_or_default().as_slice()).into(),
require_mined_proof,
})
}
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, uniffi::Object)]
#[allow(clippy::module_name_repetitions)]
pub struct ProofOutput {
pub merkle_root: Uint256,
pub nullifier_hash: Uint256,
#[serde(skip_serializing)]
pub raw_proof: Proof,
pub proof: PackedProof,
pub credential_type: CredentialType,
}
#[uniffi::export]
impl ProofOutput {
pub fn to_json(&self) -> Result<String, WalletKitError> {
serde_json::to_string(self).map_err(|e| WalletKitError::SerializationError {
error: format!("Failed to serialize proof: {e}"),
})
}
#[must_use]
pub const fn get_nullifier_hash(&self) -> Uint256 {
self.nullifier_hash
}
#[must_use]
pub const fn get_merkle_root(&self) -> Uint256 {
self.merkle_root
}
#[must_use]
pub fn get_proof_as_string(&self) -> String {
self.proof.to_string()
}
#[must_use]
pub const fn get_credential_type(&self) -> CredentialType {
self.credential_type
}
}
#[cfg(feature = "semaphore")]
pub fn generate_proof_with_semaphore_identity(
identity: &identity::Identity,
merkle_tree_proof: &MerkleTreeProof,
context: &ProofContext,
) -> Result<ProofOutput, WalletKitError> {
let merkle_root = merkle_tree_proof.merkle_root;
let external_nullifier_hash = context.external_nullifier.into();
let nullifier_hash =
generate_nullifier_hash(identity, external_nullifier_hash).into();
let proof = generate_proof(
identity,
merkle_tree_proof.as_poseidon_proof(),
external_nullifier_hash,
context.signal_hash.into(),
)?;
Ok(ProofOutput {
merkle_root,
nullifier_hash,
raw_proof: proof,
proof: PackedProof::from(proof),
credential_type: context.credential_type,
})
}
#[cfg(not(feature = "semaphore"))]
pub const fn generate_proof_with_semaphore_identity(
_identity: &identity::Identity,
_merkle_tree_proof: &MerkleTreeProof,
_context: &ProofContext,
) -> Result<ProofOutput, WalletKitError> {
Err(WalletKitError::SemaphoreNotEnabled)
}
#[cfg(test)]
mod external_nullifier_tests {
use alloy_core::primitives::address;
use ruint::{aliases::U256, uint};
use super::*;
#[test]
fn test_context_and_external_nullifier_hash_generation() {
let context = ProofContext::new(
"app_369183bd38f1641b6964ab51d7a20434",
None,
None,
CredentialType::Orb,
);
assert_eq!(
context.external_nullifier.to_padded_hex_string(),
"0x0073e4a6b670e81dc619b1f8703aa7491dc5aaadf75409aba0ac2414014c0227"
);
let context = ProofContext::new(
"app_369183bd38f1641b6964ab51d7a20434",
Some(String::new()),
None,
CredentialType::Orb,
);
assert_eq!(
context.external_nullifier.to_padded_hex_string(),
"0x0073e4a6b670e81dc619b1f8703aa7491dc5aaadf75409aba0ac2414014c0227"
);
}
#[test]
fn test_external_nullifier_hash_generation_string_action_staging() {
let context = ProofContext::new(
"app_staging_45068dca85829d2fd90e2dd6f0bff997",
Some("test-action-qli8g".to_string()),
None,
CredentialType::Orb,
);
assert_eq!(
context.external_nullifier.to_padded_hex_string(),
"0x00d8b157e767dc59faa533120ed0ce34fc51a71937292ea8baed6ee6f4fda866"
);
}
#[test]
fn test_external_nullifier_hash_generation_string_action() {
let context = ProofContext::new(
"app_10eb12bd96d8f7202892ff25f094c803",
Some("test-123123".to_string()),
None,
CredentialType::Orb,
);
assert_eq!(
context.external_nullifier.0,
uint!(
0x0065ebab05692ff2e0816cc4c3b83216c33eaa4d906c6495add6323fe0e2dc89_U256
)
);
}
#[test]
fn test_external_nullifier_hash_generation_with_advanced_abi_encoded_values() {
let custom_action = [
address!("541f3cc5772a64f2ba0a47e83236CcE2F089b188").abi_encode_packed(),
U256::from(1).abi_encode_packed(),
"hello".abi_encode_packed(),
]
.concat();
let context = ProofContext::new_from_bytes(
"app_10eb12bd96d8f7202892ff25f094c803",
Some(custom_action),
None,
CredentialType::Orb,
);
assert_eq!(
context.external_nullifier.to_padded_hex_string(),
"0x00f974ff06219e8ca992073d8bbe05084f81250dbd8f37cae733f24fcc0c5ffd"
);
}
#[test]
fn test_external_nullifier_hash_generation_with_advanced_abi_encoded_values_staging(
) {
let custom_action = [
"world".abi_encode_packed(),
U256::from(1).abi_encode_packed(),
"hello".abi_encode_packed(),
]
.concat();
let context = ProofContext::new_from_bytes(
"app_staging_45068dca85829d2fd90e2dd6f0bff997",
Some(custom_action),
None,
CredentialType::Orb,
);
assert_eq!(
context.external_nullifier.to_padded_hex_string(),
"0x005b49f95e822c7c37f4f043421689b11f880e617faa5cd0391803bc9bcc63c0"
);
}
#[cfg(feature = "legacy-nullifiers")]
#[test]
fn test_proof_generation_with_legacy_nullifier_address_book() {
let context = ProofContext::legacy_new_from_pre_image_external_nullifier(
b"internal_addressbook",
CredentialType::Device,
None,
false,
);
let expected = uint!(377593556987874043165400752883455722895901692332643678318174569531027326541_U256);
assert_eq!(
context.external_nullifier.to_padded_hex_string(),
format!("{expected:#066x}")
);
}
#[cfg(feature = "legacy-nullifiers")]
#[test]
fn test_proof_generation_with_legacy_nullifier_recurring_grant_drop() {
let grant_id = 48;
let worldchain_nullifier_hash_constant = uint!(
0x1E00000000000000000000000000000000000000000000000000000000000000_U256
);
let external_nullifier_hash =
worldchain_nullifier_hash_constant + U256::from(grant_id);
let context = ProofContext::legacy_new_from_raw_external_nullifier(
&external_nullifier_hash.into(),
CredentialType::Device,
None,
false,
)
.unwrap();
let expected = uint!(13569385457497991651199724805705614201555076328004753598373935625927319879728_U256);
assert_eq!(
context.external_nullifier.to_padded_hex_string(),
format!("{expected:#066x}")
);
}
#[cfg(feature = "legacy-nullifiers")]
#[test]
fn test_ensure_raw_external_nullifier_is_in_the_field() {
let invalid_external_nullifiers = [MODULUS, MODULUS + U256::from(1)];
for external_nullifier in invalid_external_nullifiers {
let context = ProofContext::legacy_new_from_raw_external_nullifier(
&external_nullifier.into(),
CredentialType::Device,
None,
false,
);
assert!(context.is_err());
}
}
}
#[cfg(test)]
mod signal_tests {
use ruint::aliases::U256;
use super::*;
#[test]
fn test_ensure_raw_signal_hash_is_in_the_field() {
let invalid_signals = [MODULUS, MODULUS + U256::from(1)];
for signal_hash in invalid_signals {
let context = ProofContext::new_from_signal_hash(
"my_app_id",
None,
CredentialType::Device,
&signal_hash.into(),
);
assert!(context.is_err());
}
}
#[test]
fn test_get_external_nullifier() {
let context = ProofContext::new(
"app_369183bd38f1641b6964ab51d7a20434",
Some("test-action".to_string()),
None,
CredentialType::Orb,
);
let external_nullifier = context.get_external_nullifier();
assert_eq!(external_nullifier, context.external_nullifier);
assert_eq!(
external_nullifier.to_padded_hex_string(),
"0x00dd12b56cebf29593d6d3208a061bbb19e60152c56045f277a15989d25d5215"
);
}
#[test]
fn test_get_signal_hash() {
let signal = "test_signal_123".to_string();
let context = ProofContext::new(
"app_10eb12bd96d8f7202892ff25f094c803",
None,
Some(signal.clone()),
CredentialType::Device,
);
let signal_hash = context.get_signal_hash();
assert_eq!(signal_hash, context.signal_hash);
let expected_hash = Uint256::from(hash_to_field(signal.as_bytes()));
assert_eq!(signal_hash, expected_hash);
}
#[test]
fn test_get_credential_type() {
let orb_context = ProofContext::new("app_123", None, None, CredentialType::Orb);
assert_eq!(orb_context.get_credential_type(), CredentialType::Orb);
let device_context =
ProofContext::new("app_456", None, None, CredentialType::Device);
assert_eq!(device_context.get_credential_type(), CredentialType::Device);
}
}
#[cfg(test)]
mod proof_tests {
use regex::Regex;
use semaphore_rs::protocol::verify_proof;
use serde_json::Value;
use super::*;
fn helper_load_merkle_proof() -> MerkleTreeProof {
let json_merkle: Value = serde_json::from_str(include_str!(
"../../tests/v3/fixtures/inclusion_proof.json"
))
.unwrap();
MerkleTreeProof::from_json_proof(
&serde_json::to_string(&json_merkle["proof"]).unwrap(),
json_merkle["root"].as_str().unwrap(),
)
.unwrap()
}
#[test]
fn test_proof_generation() {
let context = ProofContext::new(
"app_staging_45068dca85829d2fd90e2dd6f0bff997",
Some("test-action-89tcf".to_string()),
None,
CredentialType::Device,
);
let mut secret = b"not_a_real_secret".to_vec();
let identity = semaphore_rs::identity::Identity::from_secret(
&mut secret,
Some(context.credential_type.as_identity_trapdoor()),
);
assert_eq!(
Uint256::from(identity.commitment()).to_padded_hex_string(),
"0x1a060ef75540e13711f074b779a419c126ab5a89d2c2e7d01e64dfd121e44671"
);
let zkp = generate_proof_with_semaphore_identity(
&identity,
&helper_load_merkle_proof(),
&context,
)
.unwrap();
assert_eq!(
zkp.merkle_root.to_padded_hex_string(),
"0x2f3a95b6df9074a19bf46e2308d7f5696e9dca49e0d64ef49a1425bbf40e0c02"
);
assert_eq!(
zkp.nullifier_hash.to_padded_hex_string(),
"0x11d194ff98df5c8e239e6b6e33cce7fb1b419344cb13e064350a917970c8fea4"
);
assert!(verify_proof(
*zkp.merkle_root,
*zkp.nullifier_hash,
hash_to_field(&[]),
*context.external_nullifier,
&zkp.raw_proof,
30
)
.unwrap());
}
#[test]
fn test_proof_json_encoding() {
let context = ProofContext::new(
"app_staging_45068dca85829d2fd90e2dd6f0bff997",
Some("test-action-89tcf".to_string()),
None,
CredentialType::Device,
);
let mut secret = b"not_a_real_secret".to_vec();
let identity = semaphore_rs::identity::Identity::from_secret(
&mut secret,
Some(context.credential_type.as_identity_trapdoor()),
);
let zkp = generate_proof_with_semaphore_identity(
&identity,
&helper_load_merkle_proof(),
&context,
)
.unwrap();
let parsed_json: Value = serde_json::from_str(&zkp.to_json().unwrap()).unwrap();
assert_eq!(
parsed_json["nullifier_hash"].as_str().unwrap(),
"0x11d194ff98df5c8e239e6b6e33cce7fb1b419344cb13e064350a917970c8fea4"
);
assert_eq!(
parsed_json["merkle_root"].as_str().unwrap(),
"0x2f3a95b6df9074a19bf46e2308d7f5696e9dca49e0d64ef49a1425bbf40e0c02"
);
assert_eq!(parsed_json["credential_type"].as_str().unwrap(), "device");
let packed_proof_pattern = r"^0x[a-f0-9]{400,600}$";
let re = Regex::new(packed_proof_pattern).unwrap();
assert!(re.is_match(parsed_json["proof"].as_str().unwrap()));
assert_eq!(
zkp.get_nullifier_hash().to_padded_hex_string(),
parsed_json["nullifier_hash"].as_str().unwrap()
);
assert_eq!(
zkp.get_merkle_root().to_padded_hex_string(),
parsed_json["merkle_root"].as_str().unwrap()
);
assert_eq!(
zkp.get_proof_as_string(),
parsed_json["proof"].as_str().unwrap()
);
}
}