1use core::str::FromStr;
20
21use ark_bn254::{Bn254, Fq as BnFq, Fq12, Fr as BnFr, G1Affine as BnG1, G2Affine as BnG2};
22use ark_ec::pairing::Pairing;
23use ark_ec::{AffineRepr, CurveGroup};
24use ark_ff::{BigInteger, PrimeField, Zero};
25use ark_secp256k1::{Affine as SecpG1, Fq as SecpFq, Fr as SecpFr};
26use num_bigint::BigUint;
27#[cfg(feature = "parallel")]
28use rayon::prelude::*;
29
30use crate::encoding::from_hex;
31
32macro_rules! map_announcements {
38 ($rs:expr, $tags:expr, $f:expr) => {{
39 #[cfg(feature = "parallel")]
40 {
41 $rs.par_iter()
42 .zip($tags.par_iter())
43 .enumerate()
44 .filter_map($f)
45 .collect::<Vec<_>>()
46 }
47 #[cfg(not(feature = "parallel"))]
48 {
49 $rs.iter()
50 .zip($tags.iter())
51 .enumerate()
52 .filter_map($f)
53 .collect::<Vec<_>>()
54 }
55 }};
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct StealthError(String);
63
64impl core::fmt::Display for StealthError {
65 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
66 write!(f, "stealth core: {}", self.0)
67 }
68}
69impl std::error::Error for StealthError {}
70
71fn err(msg: impl Into<String>) -> StealthError {
72 StealthError(msg.into())
73}
74
75fn fp_to_biguint<F: PrimeField>(x: F) -> BigUint {
76 BigUint::from_bytes_be(&x.into_bigint().to_bytes_be())
77}
78fn fp_dec<F: PrimeField>(x: F) -> String {
79 fp_to_biguint(x).to_str_radix(10)
80}
81
82fn xy_bn(p: &BnG1) -> String {
83 format!("{}.{}", fp_dec(p.x().unwrap()), fp_dec(p.y().unwrap()))
84}
85fn xy_secp(p: &SecpG1) -> String {
86 format!("{}.{}", fp_dec(p.x().unwrap()), fp_dec(p.y().unwrap()))
87}
88
89fn parse_xy<F: PrimeField>(s: &str) -> Result<(F, F), StealthError> {
90 let (x, y) = s
91 .split_once('.')
92 .ok_or_else(|| err(format!("point must be \"X.Y\", got {s:?}")))?;
93 Ok((
94 F::from_str(x).map_err(|_| err(format!("bad point X: {x:?}")))?,
95 F::from_str(y).map_err(|_| err(format!("bad point Y: {y:?}")))?,
96 ))
97}
98
99fn parse_bn(s: &str, what: &str) -> Result<BnG1, StealthError> {
104 let (x, y) = parse_xy::<BnFq>(s)?;
105 let p = BnG1::new_unchecked(x, y);
106 if !p.is_on_curve() {
107 return Err(err(format!("{what} is not on BN254 G1: {s:?}")));
108 }
109 Ok(p)
110}
111fn parse_secp(s: &str, what: &str) -> Result<SecpG1, StealthError> {
112 let (x, y) = parse_xy::<SecpFq>(s)?;
113 let p = SecpG1::new_unchecked(x, y);
114 if !p.is_on_curve() {
115 return Err(err(format!("{what} is not on secp256k1: {s:?}")));
116 }
117 Ok(p)
118}
119
120fn parse_secp_scalar(hex: &str, what: &str) -> Result<SecpFr, StealthError> {
123 let s = SecpFr::from_be_bytes_mod_order(&from_hex(hex));
124 if s.is_zero() {
125 return Err(err(format!("{what} reduces to zero")));
126 }
127 Ok(s)
128}
129fn parse_bn_scalar(hex: &str, what: &str) -> Result<BnFr, StealthError> {
130 let v = BnFr::from_be_bytes_mod_order(&from_hex(hex));
131 if v.is_zero() {
132 return Err(err(format!("{what} reduces to zero")));
133 }
134 Ok(v)
135}
136
137fn bn_mul(p: BnG1, scalar: BnFr) -> BnG1 {
138 (p.into_group() * scalar).into_affine()
139}
140fn secp_mul(p: SecpG1, scalar: SecpFr) -> SecpG1 {
141 (p.into_group() * scalar).into_affine()
142}
143
144fn compute_b(secret: &Fq12) -> SecpFr {
146 let a0: BnFq = secret.c0.c0.c0; SecpFr::from_le_bytes_mod_order(&a0.into_bigint().to_bytes_le())
148}
149
150fn view_tag(p: &BnG1) -> String {
152 fp_to_biguint(p.x().unwrap())
153 .to_str_radix(16)
154 .chars()
155 .take(2)
156 .collect()
157}
158
159fn tag_matches(vri: &BnG1, vt: &str) -> bool {
166 vt.get(..2).is_some_and(|prefix| view_tag(vri) == prefix)
167}
168
169pub fn get_meta(k_hex: &str, v_hex: &str) -> Result<(String, String), StealthError> {
171 let s = parse_secp_scalar(k_hex, "spend private key")?;
172 let big_s = secp_mul(SecpG1::generator(), s);
173 let v = parse_bn_scalar(v_hex, "view private key")?;
174 let big_v = bn_mul(BnG1::generator(), v);
175 Ok((xy_secp(&big_s), xy_bn(&big_v)))
176}
177
178pub struct SendOutput {
181 pub big_r: String,
182 pub view_tag: String,
183 pub spending_pub_key: String,
184}
185
186pub fn send_with_r(r_dec: &str, big_k: &str, big_v: &str) -> Result<SendOutput, StealthError> {
187 let r = BnFr::from_str(r_dec).map_err(|_| err(format!("bad ephemeral r: {r_dec:?}")))?;
188 if r.is_zero() {
189 return Err(err("ephemeral r must be nonzero"));
190 }
191 let big_v_pt = parse_bn(big_v, "recipient view key V")?;
195 let big_k_pt = parse_secp(big_k, "recipient spend key K")?;
196 let big_r = bn_mul(BnG1::generator(), r);
197 let rv = bn_mul(big_v_pt, r);
198 let secret = Bn254::pairing(rv, BnG2::generator());
199 let b = compute_b(&secret.0);
200 let spk = secp_mul(big_k_pt, b);
201 Ok(SendOutput {
202 big_r: xy_bn(&big_r),
203 view_tag: view_tag(&rv),
204 spending_pub_key: xy_secp(&spk),
205 })
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct ScanMatch {
214 pub index: u32,
215 pub spending_pub_key: String,
216 pub spending_priv_key: String,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct ViewerMatch {
222 pub index: u32,
223 pub spending_pub_key: String,
224}
225
226pub fn scan(
232 k_hex: &str,
233 v_hex: &str,
234 rs: &[String],
235 view_tags: &[String],
236) -> Result<Vec<ScanMatch>, StealthError> {
237 if rs.len() != view_tags.len() {
238 return Err(err(format!(
239 "Rs.len ({}) != viewTags.len ({})",
240 rs.len(),
241 view_tags.len()
242 )));
243 }
244 let s = parse_secp_scalar(k_hex, "spend private key")?;
245 let big_s = secp_mul(SecpG1::generator(), s);
246 let v = parse_bn_scalar(v_hex, "view private key")?;
247
248 Ok(map_announcements!(rs, view_tags, |(i, (ri_str, vt)): (
249 usize,
250 (&String, &String)
251 )| {
252 let ri = parse_bn(ri_str, "announcement R").ok()?;
253 let vri = bn_mul(ri, v);
256 if !tag_matches(&vri, vt) {
257 return None;
258 }
259 let b = compute_b(&Bn254::pairing(vri, BnG2::generator()).0);
260 let sb = s * b;
261 Some(ScanMatch {
262 index: i as u32,
263 spending_pub_key: xy_secp(&secp_mul(big_s, b)),
264 spending_priv_key: format!("0x{}", fp_to_biguint(sb).to_str_radix(16)),
265 })
266 }))
267}
268
269pub fn viewer_scan(
273 v_hex: &str,
274 big_s: &str,
275 rs: &[String],
276 view_tags: &[String],
277) -> Result<Vec<ViewerMatch>, StealthError> {
278 if rs.len() != view_tags.len() {
279 return Err(err(format!(
280 "Rs.len ({}) != viewTags.len ({})",
281 rs.len(),
282 view_tags.len()
283 )));
284 }
285 let v = parse_bn_scalar(v_hex, "view private key")?;
286 let s = parse_secp(big_s, "spend public key S")?;
287 Ok(map_announcements!(rs, view_tags, |(i, (ri_str, vt)): (
288 usize,
289 (&String, &String)
290 )| {
291 let ri = parse_bn(ri_str, "announcement R").ok()?;
292 let vri = bn_mul(ri, v);
293 if !tag_matches(&vri, vt) {
294 return None;
295 }
296 let b = compute_b(&Bn254::pairing(vri, BnG2::generator()).0);
297 Some(ViewerMatch {
298 index: i as u32,
299 spending_pub_key: xy_secp(&secp_mul(s, b)),
300 })
301 }))
302}
303
304fn random_scalar_bytes() -> Result<[u8; 32], StealthError> {
305 let mut b = [0u8; 32];
306 getrandom::getrandom(&mut b)
307 .map_err(|error| err(format!("secure randomness unavailable: {error}")))?;
308 Ok(b)
309}
310
311fn pad_even(s: &str) -> String {
312 if s.len().is_multiple_of(2) {
313 s.to_string()
314 } else {
315 format!("0{s}")
316 }
317}
318
319fn random_nonzero<F: PrimeField>() -> Result<F, StealthError> {
320 loop {
322 let x = F::from_le_bytes_mod_order(&random_scalar_bytes()?);
323 if !x.is_zero() {
324 return Ok(x);
325 }
326 }
327}
328
329pub fn new_meta() -> Result<(String, String, String, String), StealthError> {
332 let s = random_nonzero::<SecpFr>()?;
333 let v = random_nonzero::<BnFr>()?;
334 let k_hex = pad_even(&fp_to_biguint(s).to_str_radix(16));
335 let v_hex = pad_even(&fp_to_biguint(v).to_str_radix(16));
336 Ok((
337 k_hex,
338 v_hex,
339 xy_secp(&secp_mul(SecpG1::generator(), s)),
340 xy_bn(&bn_mul(BnG1::generator(), v)),
341 ))
342}
343
344pub fn send(big_k: &str, big_v: &str) -> Result<(String, SendOutput), StealthError> {
347 let r = random_nonzero::<BnFr>()?;
348 let r_dec = fp_to_biguint(r).to_str_radix(10);
349 let out = send_with_r(&r_dec, big_k, big_v)?;
350 Ok((r_dec, out))
351}
352
353pub fn is_valid_bn254_point(point: &str) -> bool {
355 parse_bn(point, "point").is_ok()
356}
357
358pub fn is_valid_secp256k1_point(point: &str) -> bool {
360 parse_secp(point, "point").is_ok()
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 #[test]
368 fn new_meta_round_trips_through_get_meta() {
369 let (k, v, big_k, big_v) = new_meta().unwrap();
372 let (rk, rv) = get_meta(&k, &v).unwrap();
373 assert_eq!((rk, rv), (big_k.clone(), big_v.clone()));
374
375 let (_r, sent) = send(&big_k, &big_v).unwrap();
376 let found = scan(&k, &v, &[sent.big_r], &[sent.view_tag]).unwrap();
377 assert_eq!(found.len(), 1);
378 assert_eq!(found[0].index, 0);
379 assert_eq!(found[0].spending_pub_key, sent.spending_pub_key);
380 assert!(found[0].spending_priv_key.starts_with("0x"));
381 }
382
383 const OFF_CURVE: &str = "1.3";
385
386 #[test]
387 fn scan_skips_bad_announcements_without_aborting() {
388 let (k, v, big_k, big_v) = new_meta().unwrap();
389 let (_r, sent) = send(&big_k, &big_v).unwrap();
390
391 let rs = vec![
392 OFF_CURVE.to_string(), "not-a-point".to_string(), sent.big_r.clone(), sent.big_r.clone(), ];
397 let tags = vec!["ab".into(), "cd".into(), sent.view_tag.clone(), "a".into()];
398
399 let found = scan(&k, &v, &rs, &tags).unwrap();
400 assert_eq!(found.len(), 1, "only the real announcement matches");
401 assert_eq!(found[0].index, 2);
402 assert_eq!(found[0].spending_pub_key, sent.spending_pub_key);
403
404 let seen = viewer_scan(&v, &big_k, &rs, &tags).unwrap();
405 assert_eq!(seen.len(), 1);
406 assert_eq!(
407 (seen[0].index, seen[0].spending_pub_key.as_str()),
408 (2, sent.spending_pub_key.as_str())
409 );
410 }
411
412 #[test]
413 fn send_rejects_malformed_recipient_keys() {
414 let (_k, _v, big_k, big_v) = new_meta().unwrap();
415 assert!(
416 send(OFF_CURVE, &big_v).is_err(),
417 "off-curve K must be rejected"
418 );
419 assert!(
420 send(&big_k, OFF_CURVE).is_err(),
421 "off-curve V must be rejected"
422 );
423 assert!(send("garbage", &big_v).is_err());
424 assert!(
425 send_with_r("0", &big_k, &big_v).is_err(),
426 "zero ephemeral r must be rejected"
427 );
428 }
429
430 #[test]
431 fn own_key_and_shape_errors_are_hard() {
432 let (k, v, _big_k, _big_v) = new_meta().unwrap();
433 assert!(get_meta("00", &v).is_err(), "zero spend key");
434 assert!(get_meta(&k, "00").is_err(), "zero view key");
435 assert!(
436 scan(&k, &v, &["1.2".into()], &[]).is_err(),
437 "length mismatch"
438 );
439 assert!(viewer_scan(&v, OFF_CURVE, &[], &[]).is_err(), "off-curve S");
440 }
441
442 #[test]
443 fn point_validators_reject_off_curve_and_garbage() {
444 assert!(is_valid_bn254_point("1.2")); assert!(!is_valid_bn254_point(OFF_CURVE));
446 assert!(!is_valid_bn254_point("1.2.3"));
447 assert!(!is_valid_secp256k1_point("1.3"));
448 assert!(!is_valid_secp256k1_point(""));
449 }
450}