rustywallet_silent/
scanner.rs

1//! Silent Payment scanning for receivers.
2
3use crate::error::{Result, SilentPaymentError};
4use crate::label::{Label, LabelManager};
5use secp256k1::{PublicKey, Secp256k1, SecretKey};
6use sha2::{Digest, Sha256};
7
8/// BIP352 shared secret tag.
9const SHARED_SECRET_TAG: &[u8] = b"BIP0352/SharedSecret";
10
11/// A detected Silent Payment.
12#[derive(Debug, Clone)]
13pub struct DetectedPayment {
14    /// Output public key (x-only, 32 bytes)
15    pub output_pubkey: [u8; 32],
16    /// Spending private key for this output
17    pub spending_key: [u8; 32],
18    /// Output index in the transaction
19    pub output_index: usize,
20    /// Label if matched (None for unlabeled)
21    pub label: Option<u32>,
22}
23
24/// Scanner for detecting Silent Payments.
25pub struct SilentPaymentScanner {
26    /// Scan private key
27    scan_privkey: [u8; 32],
28    /// Spend private key
29    spend_privkey: [u8; 32],
30    /// Spend public key
31    spend_pubkey: [u8; 33],
32    /// Label manager
33    labels: LabelManager,
34}
35
36impl SilentPaymentScanner {
37    /// Create a new scanner.
38    pub fn new(scan_privkey: &[u8; 32], spend_privkey: &[u8; 32]) -> Result<Self> {
39        let secp = Secp256k1::new();
40
41        let spend_sk = SecretKey::from_slice(spend_privkey)
42            .map_err(|e| SilentPaymentError::InvalidPrivateKey(e.to_string()))?;
43        let spend_pk = PublicKey::from_secret_key(&secp, &spend_sk);
44
45        Ok(Self {
46            scan_privkey: *scan_privkey,
47            spend_privkey: *spend_privkey,
48            spend_pubkey: spend_pk.serialize(),
49            labels: LabelManager::new(),
50        })
51    }
52
53    /// Add a label to scan for.
54    pub fn add_label(&mut self, index: u32) {
55        self.labels.add(index);
56    }
57
58    /// Add multiple labels.
59    pub fn add_labels(&mut self, count: u32) {
60        self.labels.generate_range(count);
61    }
62
63    /// Scan transaction outputs for payments.
64    ///
65    /// # Arguments
66    /// * `output_pubkeys` - X-only public keys of transaction outputs
67    /// * `input_pubkeys` - Public keys of transaction inputs
68    /// * `outpoints` - (txid, vout) pairs for inputs
69    pub fn scan(
70        &self,
71        output_pubkeys: &[[u8; 32]],
72        input_pubkeys: &[[u8; 33]],
73        outpoints: &[([u8; 32], u32)],
74    ) -> Result<Vec<DetectedPayment>> {
75        if input_pubkeys.is_empty() || outpoints.is_empty() {
76            return Ok(Vec::new());
77        }
78
79        let secp = Secp256k1::new();
80
81        // Compute sum of input public keys
82        let a_sum = sum_public_keys(input_pubkeys)?;
83
84        // Compute input hash
85        let input_hash = compute_input_hash(outpoints, &a_sum)?;
86
87        // Compute A_sum * input_hash
88        let a_sum_pk = PublicKey::from_slice(&a_sum)
89            .map_err(|e| SilentPaymentError::InvalidPublicKey(e.to_string()))?;
90        let input_hash_sk = SecretKey::from_slice(&input_hash)
91            .map_err(|e| SilentPaymentError::CryptoError(e.to_string()))?;
92        let tweaked_a = a_sum_pk
93            .mul_tweak(&secp, &input_hash_sk.into())
94            .map_err(|e| SilentPaymentError::CryptoError(e.to_string()))?;
95
96        // Compute shared secret: ECDH(b_scan, A_sum * input_hash)
97        let b_scan = SecretKey::from_slice(&self.scan_privkey)
98            .map_err(|e| SilentPaymentError::InvalidPrivateKey(e.to_string()))?;
99        let shared_secret_point = tweaked_a
100            .mul_tweak(&secp, &b_scan.into())
101            .map_err(|e| SilentPaymentError::CryptoError(e.to_string()))?;
102
103        let mut detected = Vec::new();
104
105        // Try to match each output
106        for (output_idx, output_pk) in output_pubkeys.iter().enumerate() {
107            // Try k = 0, 1, 2, ... until no match
108            for k in 0..100 {
109                // Reasonable limit
110                let t_k = compute_output_tweak(&shared_secret_point.serialize(), k);
111
112                // Check unlabeled: P = B_spend + t_k * G
113                if let Some(payment) =
114                    self.try_match_output(output_pk, &t_k, output_idx, None, &secp)?
115                {
116                    detected.push(payment);
117                    break;
118                }
119
120                // Check labeled outputs
121                let mut found_label = false;
122                for label in self.labels.labels() {
123                    if let Some(payment) =
124                        self.try_match_labeled_output(output_pk, &t_k, output_idx, label, &secp)?
125                    {
126                        detected.push(payment);
127                        found_label = true;
128                        break;
129                    }
130                }
131
132                if found_label {
133                    break;
134                }
135
136                // If k > 0 and no match, stop trying higher k values
137                if k > 0 {
138                    break;
139                }
140            }
141        }
142
143        Ok(detected)
144    }
145
146    /// Try to match an output without label.
147    fn try_match_output(
148        &self,
149        output_pk: &[u8; 32],
150        t_k: &[u8; 32],
151        output_idx: usize,
152        label: Option<u32>,
153        secp: &Secp256k1<secp256k1::All>,
154    ) -> Result<Option<DetectedPayment>> {
155        let b_spend = PublicKey::from_slice(&self.spend_pubkey)
156            .map_err(|e| SilentPaymentError::InvalidPublicKey(e.to_string()))?;
157
158        let t_k_sk = SecretKey::from_slice(t_k)
159            .map_err(|e| SilentPaymentError::CryptoError(e.to_string()))?;
160        let t_k_point = PublicKey::from_secret_key(secp, &t_k_sk);
161
162        let expected_pk = b_spend
163            .combine(&t_k_point)
164            .map_err(|e| SilentPaymentError::CryptoError(e.to_string()))?;
165
166        let (expected_xonly, _parity) = expected_pk.x_only_public_key();
167
168        if expected_xonly.serialize() == *output_pk {
169            // Compute spending key: b_spend + t_k
170            let b_spend_sk = SecretKey::from_slice(&self.spend_privkey)
171                .map_err(|e| SilentPaymentError::InvalidPrivateKey(e.to_string()))?;
172            let spending_key = b_spend_sk
173                .add_tweak(&t_k_sk.into())
174                .map_err(|e| SilentPaymentError::CryptoError(e.to_string()))?;
175
176            return Ok(Some(DetectedPayment {
177                output_pubkey: *output_pk,
178                spending_key: spending_key.secret_bytes(),
179                output_index: output_idx,
180                label,
181            }));
182        }
183
184        Ok(None)
185    }
186
187    /// Try to match a labeled output.
188    fn try_match_labeled_output(
189        &self,
190        output_pk: &[u8; 32],
191        t_k: &[u8; 32],
192        output_idx: usize,
193        label: &Label,
194        secp: &Secp256k1<secp256k1::All>,
195    ) -> Result<Option<DetectedPayment>> {
196        // B_m = B_spend + label * G
197        let b_m = label.apply_to_pubkey(&self.spend_pubkey)?;
198
199        let b_m_pk = PublicKey::from_slice(&b_m)
200            .map_err(|e| SilentPaymentError::InvalidPublicKey(e.to_string()))?;
201
202        let t_k_sk = SecretKey::from_slice(t_k)
203            .map_err(|e| SilentPaymentError::CryptoError(e.to_string()))?;
204        let t_k_point = PublicKey::from_secret_key(secp, &t_k_sk);
205
206        let expected_pk = b_m_pk
207            .combine(&t_k_point)
208            .map_err(|e| SilentPaymentError::CryptoError(e.to_string()))?;
209
210        let (expected_xonly, _parity) = expected_pk.x_only_public_key();
211
212        if expected_xonly.serialize() == *output_pk {
213            // Compute spending key: b_spend + label + t_k
214            let b_m_sk = label.apply_to_privkey(&self.spend_privkey)?;
215            let b_m_secret = SecretKey::from_slice(&b_m_sk)
216                .map_err(|e| SilentPaymentError::InvalidPrivateKey(e.to_string()))?;
217            let spending_key = b_m_secret
218                .add_tweak(&t_k_sk.into())
219                .map_err(|e| SilentPaymentError::CryptoError(e.to_string()))?;
220
221            return Ok(Some(DetectedPayment {
222                output_pubkey: *output_pk,
223                spending_key: spending_key.secret_bytes(),
224                output_index: output_idx,
225                label: Some(label.index()),
226            }));
227        }
228
229        Ok(None)
230    }
231}
232
233/// Light scanner using only scan key (cannot compute spending keys).
234pub struct LightScanner {
235    /// Scan private key
236    scan_privkey: [u8; 32],
237    /// Spend public key
238    spend_pubkey: [u8; 33],
239}
240
241impl LightScanner {
242    /// Create a new light scanner.
243    pub fn new(scan_privkey: &[u8; 32], spend_pubkey: &[u8; 33]) -> Result<Self> {
244        // Validate keys
245        SecretKey::from_slice(scan_privkey)
246            .map_err(|e| SilentPaymentError::InvalidPrivateKey(e.to_string()))?;
247        PublicKey::from_slice(spend_pubkey)
248            .map_err(|e| SilentPaymentError::InvalidPublicKey(e.to_string()))?;
249
250        Ok(Self {
251            scan_privkey: *scan_privkey,
252            spend_pubkey: *spend_pubkey,
253        })
254    }
255
256    /// Check if an output belongs to this address.
257    pub fn check_output(
258        &self,
259        output_pk: &[u8; 32],
260        input_pubkeys: &[[u8; 33]],
261        outpoints: &[([u8; 32], u32)],
262    ) -> Result<bool> {
263        if input_pubkeys.is_empty() || outpoints.is_empty() {
264            return Ok(false);
265        }
266
267        let secp = Secp256k1::new();
268
269        let a_sum = sum_public_keys(input_pubkeys)?;
270        let input_hash = compute_input_hash(outpoints, &a_sum)?;
271
272        let a_sum_pk = PublicKey::from_slice(&a_sum)
273            .map_err(|e| SilentPaymentError::InvalidPublicKey(e.to_string()))?;
274        let input_hash_sk = SecretKey::from_slice(&input_hash)
275            .map_err(|e| SilentPaymentError::CryptoError(e.to_string()))?;
276        let tweaked_a = a_sum_pk
277            .mul_tweak(&secp, &input_hash_sk.into())
278            .map_err(|e| SilentPaymentError::CryptoError(e.to_string()))?;
279
280        let b_scan = SecretKey::from_slice(&self.scan_privkey)
281            .map_err(|e| SilentPaymentError::InvalidPrivateKey(e.to_string()))?;
282        let shared_secret_point = tweaked_a
283            .mul_tweak(&secp, &b_scan.into())
284            .map_err(|e| SilentPaymentError::CryptoError(e.to_string()))?;
285
286        let t_0 = compute_output_tweak(&shared_secret_point.serialize(), 0);
287
288        let b_spend = PublicKey::from_slice(&self.spend_pubkey)
289            .map_err(|e| SilentPaymentError::InvalidPublicKey(e.to_string()))?;
290        let t_0_sk = SecretKey::from_slice(&t_0)
291            .map_err(|e| SilentPaymentError::CryptoError(e.to_string()))?;
292        let t_0_point = PublicKey::from_secret_key(&secp, &t_0_sk);
293
294        let expected_pk = b_spend
295            .combine(&t_0_point)
296            .map_err(|e| SilentPaymentError::CryptoError(e.to_string()))?;
297
298        let (expected_xonly, _parity) = expected_pk.x_only_public_key();
299
300        Ok(expected_xonly.serialize() == *output_pk)
301    }
302}
303
304/// Sum public keys.
305fn sum_public_keys(keys: &[[u8; 33]]) -> Result<[u8; 33]> {
306    if keys.is_empty() {
307        return Err(SilentPaymentError::NoInputs);
308    }
309
310    let mut sum = PublicKey::from_slice(&keys[0])
311        .map_err(|e| SilentPaymentError::InvalidPublicKey(e.to_string()))?;
312
313    for key in &keys[1..] {
314        let pk = PublicKey::from_slice(key)
315            .map_err(|e| SilentPaymentError::InvalidPublicKey(e.to_string()))?;
316        sum = sum
317            .combine(&pk)
318            .map_err(|e| SilentPaymentError::CryptoError(e.to_string()))?;
319    }
320
321    Ok(sum.serialize())
322}
323
324/// Compute input hash.
325fn compute_input_hash(outpoints: &[([u8; 32], u32)], a_sum: &[u8; 33]) -> Result<[u8; 32]> {
326    let mut sorted: Vec<_> = outpoints.to_vec();
327    sorted.sort_by(|a, b| {
328        let cmp = a.0.cmp(&b.0);
329        if cmp == std::cmp::Ordering::Equal {
330            a.1.cmp(&b.1)
331        } else {
332            cmp
333        }
334    });
335
336    let mut hasher = Sha256::new();
337    hasher.update(sorted[0].0);
338    hasher.update(sorted[0].1.to_le_bytes());
339    hasher.update(a_sum);
340
341    let result = hasher.finalize();
342    let mut hash = [0u8; 32];
343    hash.copy_from_slice(&result);
344
345    Ok(hash)
346}
347
348/// Compute output tweak.
349fn compute_output_tweak(shared_secret: &[u8; 33], k: u32) -> [u8; 32] {
350    let tag_hash = Sha256::digest(SHARED_SECRET_TAG);
351
352    let mut hasher = Sha256::new();
353    hasher.update(tag_hash);
354    hasher.update(tag_hash);
355    hasher.update(shared_secret);
356    hasher.update(k.to_be_bytes());
357
358    let result = hasher.finalize();
359    let mut tweak = [0u8; 32];
360    tweak.copy_from_slice(&result);
361    tweak
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367    use crate::address::SilentPaymentAddress;
368    use crate::network::Network;
369    use crate::sender::create_outputs;
370    use rustywallet_keys::private_key::PrivateKey;
371
372    #[test]
373    fn test_scanner_creation() {
374        let scan_key = PrivateKey::random();
375        let spend_key = PrivateKey::random();
376
377        let scanner =
378            SilentPaymentScanner::new(&scan_key.to_bytes(), &spend_key.to_bytes()).unwrap();
379
380        assert!(scanner.labels.labels().is_empty());
381    }
382
383    #[test]
384    fn test_light_scanner() {
385        let scan_key = PrivateKey::random();
386        let spend_key = PrivateKey::random();
387
388        let scanner = LightScanner::new(
389            &scan_key.to_bytes(),
390            &spend_key.public_key().to_compressed().try_into().unwrap(),
391        )
392        .unwrap();
393
394        // Just verify it was created
395        assert_eq!(scanner.spend_pubkey.len(), 33);
396    }
397
398    #[test]
399    fn test_end_to_end_payment() {
400        // Sender setup
401        let sender_key = PrivateKey::random();
402        let sender_pubkey: [u8; 33] = sender_key
403            .public_key()
404            .to_compressed()
405            .try_into()
406            .unwrap();
407
408        // Receiver setup
409        let scan_key = PrivateKey::random();
410        let spend_key = PrivateKey::random();
411
412        let sp_address = SilentPaymentAddress::new(
413            &scan_key.public_key(),
414            &spend_key.public_key(),
415            Network::Mainnet,
416        )
417        .unwrap();
418
419        // Create payment
420        let outpoints = vec![([1u8; 32], 0u32)];
421        let outputs =
422            create_outputs(&[sender_key.to_bytes()], &outpoints, &[sp_address]).unwrap();
423
424        assert_eq!(outputs.len(), 1);
425
426        // Scan for payment
427        let scanner =
428            SilentPaymentScanner::new(&scan_key.to_bytes(), &spend_key.to_bytes()).unwrap();
429
430        let detected = scanner
431            .scan(&[outputs[0].output_pubkey], &[sender_pubkey], &outpoints)
432            .unwrap();
433
434        assert_eq!(detected.len(), 1);
435        assert_eq!(detected[0].output_pubkey, outputs[0].output_pubkey);
436        assert!(detected[0].label.is_none());
437
438        // Verify spending key produces correct public key
439        let secp = Secp256k1::new();
440        let spending_sk = SecretKey::from_slice(&detected[0].spending_key).unwrap();
441        let spending_pk = PublicKey::from_secret_key(&secp, &spending_sk);
442        let (xonly, _) = spending_pk.x_only_public_key();
443
444        assert_eq!(xonly.serialize(), outputs[0].output_pubkey);
445    }
446}