use ootle_byte_type::ConvertFromByteType;
use tari_crypto::ristretto::pedersen::PedersenCommitment;
use tari_engine_types::crypto::{ValueLookupTable, validate_elgamal_verifiable_balance_proof};
use tari_ootle_wallet_crypto::{
GenerateValueLookup,
viewable_balance_proof::generate_elgamal_viewable_balance_proof as crypto_generate,
};
use tari_template_lib_types::stealth::ViewableBalanceProof;
use crate::{
error::OotleWasmError,
keys::{commitment_bytes_from_bytes, public_key_from_bytes, secret_key_from_bytes},
};
pub fn generate_elgamal_viewable_balance_proof(
mask: &[u8],
amount: u64,
commitment: &[u8],
view_public_key: &[u8],
) -> Result<String, OotleWasmError> {
let mask = secret_key_from_bytes(mask)?;
let commitment_bytes = commitment_bytes_from_bytes(commitment)?;
let commitment = PedersenCommitment::convert_from_byte_type(&commitment_bytes)
.map_err(|e| OotleWasmError::InvalidCommitment(e.to_string()))?;
let view_key = public_key_from_bytes(view_public_key)?;
let proof =
crypto_generate(&mask, amount, &commitment, &view_key).map_err(|e| OotleWasmError::Stealth(e.to_string()))?;
Ok(serde_json::to_string(&proof)?)
}
pub fn decrypt_elgamal_viewable_balance(
proof_json: &str,
commitment: &[u8],
view_public_key: &[u8],
view_secret_key: &[u8],
min_value: u64,
max_value: u64,
) -> Result<Option<u64>, OotleWasmError> {
if max_value < min_value {
return Err(OotleWasmError::Stealth(format!(
"max_value ({max_value}) must be >= min_value ({min_value})"
)));
}
let proof: ViewableBalanceProof = serde_json::from_str(proof_json)?;
let commitment_bytes = commitment_bytes_from_bytes(commitment)?;
let commitment = PedersenCommitment::convert_from_byte_type(&commitment_bytes)
.map_err(|e| OotleWasmError::InvalidCommitment(e.to_string()))?;
let view_pk = public_key_from_bytes(view_public_key)?;
let view_sk = secret_key_from_bytes(view_secret_key)?;
let verifiable = validate_elgamal_verifiable_balance_proof(&commitment, Some(&view_pk), Some(&proof))
.map_err(|e| OotleWasmError::Stealth(e.to_string()))?
.ok_or_else(|| OotleWasmError::Stealth("ElGamal proof returned no verifiable balance".to_string()))?;
let mut lookup: GenerateValueLookup = GenerateValueLookup;
let result: Result<Option<u64>, <GenerateValueLookup as ValueLookupTable>::Error> =
verifiable.brute_force_balance(&view_sk, min_value..=max_value, &mut lookup);
let value = result.map_err(|e| OotleWasmError::Stealth(format!("Value lookup failed: {e}")))?;
Ok(value)
}
#[cfg(test)]
mod tests {
use ootle_byte_type::ToByteType;
use tari_crypto::{
commitment::HomomorphicCommitmentFactory,
keys::{PublicKey, SecretKey},
ristretto::{RistrettoPublicKey, RistrettoSecretKey},
tari_utilities::ByteArray,
};
use tari_engine_types::crypto::get_commitment_factory;
use super::*;
fn build_proof(
amount: u64,
) -> (
RistrettoSecretKey,
RistrettoPublicKey,
RistrettoSecretKey,
Vec<u8>,
String,
) {
let mut rng = rand::rng();
let mask = RistrettoSecretKey::random(&mut rng);
let (view_sk, view_pk) = RistrettoPublicKey::random_keypair(&mut rng);
let commitment = get_commitment_factory().commit_value(&mask, amount);
let commitment_bytes = commitment.to_byte_type();
let proof_json = generate_elgamal_viewable_balance_proof(
mask.as_bytes(),
amount,
commitment_bytes.as_bytes(),
view_pk.as_bytes(),
)
.unwrap();
(mask, view_pk, view_sk, commitment_bytes.as_bytes().to_vec(), proof_json)
}
#[test]
fn round_trip_with_view_key_holder() {
let amount = 42u64;
let (_mask, view_pk, view_sk, commitment, proof_json) = build_proof(amount);
let decoded =
decrypt_elgamal_viewable_balance(&proof_json, &commitment, view_pk.as_bytes(), view_sk.as_bytes(), 0, 100)
.unwrap();
assert_eq!(decoded, Some(amount));
}
#[test]
fn returns_none_when_value_is_outside_range() {
let amount = 200u64;
let (_mask, view_pk, view_sk, commitment, proof_json) = build_proof(amount);
let decoded =
decrypt_elgamal_viewable_balance(&proof_json, &commitment, view_pk.as_bytes(), view_sk.as_bytes(), 0, 100)
.unwrap();
assert_eq!(decoded, None);
}
#[test]
fn rejects_inverted_range() {
let (_mask, view_pk, view_sk, commitment, proof_json) = build_proof(0);
let err =
decrypt_elgamal_viewable_balance(&proof_json, &commitment, view_pk.as_bytes(), view_sk.as_bytes(), 10, 5)
.unwrap_err();
assert!(matches!(err, OotleWasmError::Stealth(_)));
}
}