Skip to main content

silent_payments_receive/
label.rs

1//! Label management for Silent Payments receiving with O(1) lookup.
2//!
3//! [`LabelManager`] wraps a `HashMap<PublicKey, (Scalar, u32)>` for O(1)
4//! per-output label matching (RECV-02). Internally converts to `BTreeMap`
5//! when passing to bdk-sp's `scan_txouts`.
6//!
7//! The HashMap key is the label public key (`label_scalar * G`), which
8//! bdk-sp's scanner subtracts from candidate outputs to check for label
9//! matches.
10
11use std::collections::{BTreeMap, HashMap};
12
13use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey};
14
15use silent_payments_core::error::CryptoError;
16use silent_payments_core::keys::{ScanSecretKey, SpendPublicKey};
17
18/// Manages labeled Silent Payment addresses with O(1) per-output lookup.
19///
20/// BIP 352 labels allow a receiver to generate multiple addresses from a
21/// single scan/spend key pair. Each label index `m` produces a distinct
22/// address with a deterministic tweak.
23///
24/// Internally stores `HashMap<PublicKey, (Scalar, u32)>` where:
25/// - Key: `label_scalar * G` (the label public key)
26/// - Value: `(label_scalar, label_index_m)`
27#[derive(Debug, Clone)]
28pub struct LabelManager {
29    labels: HashMap<PublicKey, (Scalar, u32)>,
30}
31
32impl LabelManager {
33    /// Create an empty label manager.
34    pub fn new() -> Self {
35        Self {
36            labels: HashMap::new(),
37        }
38    }
39
40    /// Register a label index `m` for scanning.
41    ///
42    /// Computes the label tweak scalar via `BIP0352/Label(b_scan || m)`,
43    /// then derives the label public key (`label_scalar * G`) and stores
44    /// it for O(1) lookup during output scanning.
45    ///
46    /// # Errors
47    ///
48    /// Returns [`CryptoError`] if the label tweak produces an invalid
49    /// secret key (computationally unreachable).
50    pub fn add_label(
51        &mut self,
52        scan_secret: &ScanSecretKey,
53        _spend_pubkey: &SpendPublicKey,
54        m: u32,
55        secp: &Secp256k1<bitcoin::secp256k1::All>,
56    ) -> Result<(), CryptoError> {
57        // Compute label_scalar = hash(b_scan || m) via BIP0352/Label tagged hash
58        let label_scalar = bdk_sp::hashes::get_label_tweak(*scan_secret.as_inner(), m);
59
60        // Compute label_pubkey = label_scalar * G
61        let label_sk = SecretKey::from_slice(&label_scalar.to_be_bytes())
62            .map_err(|_| CryptoError::ZeroTweakResult)?;
63        let label_pubkey = label_sk.public_key(secp);
64
65        self.labels.insert(label_pubkey, (label_scalar, m));
66        Ok(())
67    }
68
69    /// Check if a label index `m` has been registered.
70    pub fn contains(&self, m: u32) -> bool {
71        self.labels.values().any(|(_, label_m)| *label_m == m)
72    }
73
74    /// Number of registered labels.
75    pub fn len(&self) -> usize {
76        self.labels.len()
77    }
78
79    /// Whether the manager has no registered labels.
80    pub fn is_empty(&self) -> bool {
81        self.labels.is_empty()
82    }
83
84    /// Convert to `BTreeMap` for bdk-sp consumption.
85    ///
86    /// This is an O(n log n) operation but only happens once per
87    /// `scan_transaction` call, not per output.
88    pub fn to_btree(&self) -> BTreeMap<PublicKey, (Scalar, u32)> {
89        self.labels.iter().map(|(k, v)| (*k, *v)).collect()
90    }
91}
92
93impl Default for LabelManager {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use silent_payments_core::keys::{ScanSecretKey, SpendSecretKey};
103
104    // Known valid secret key bytes
105    const SCAN_SK_BYTES: [u8; 32] = [
106        0xea, 0xdc, 0x78, 0x16, 0x5f, 0xf1, 0xf8, 0xea, 0x94, 0xad, 0x7c, 0xfd, 0xc5, 0x49, 0x90,
107        0x73, 0x8a, 0x4c, 0x53, 0xf6, 0xe0, 0x50, 0x7b, 0x42, 0x15, 0x42, 0x01, 0xb8, 0xe5, 0xdf,
108        0xf3, 0xb1,
109    ];
110
111    const SPEND_SK_BYTES: [u8; 32] = [
112        0x93, 0xf5, 0xed, 0x90, 0x7a, 0xd5, 0xb2, 0xbd, 0xbb, 0xdc, 0xb5, 0xd9, 0x11, 0x6e, 0xbc,
113        0x0a, 0x4e, 0x1f, 0x92, 0xf9, 0x10, 0xd5, 0x26, 0x02, 0x37, 0xfa, 0x45, 0xa9, 0x40, 0x8a,
114        0xad, 0x16,
115    ];
116
117    fn test_keys() -> (ScanSecretKey, SpendPublicKey) {
118        let secp = Secp256k1::new();
119        let scan_sk = ScanSecretKey::from_slice(&SCAN_SK_BYTES).unwrap();
120        let spend_sk = SpendSecretKey::from_slice(&SPEND_SK_BYTES).unwrap();
121        let spend_pk = spend_sk.public_key(&secp);
122        (scan_sk, spend_pk)
123    }
124
125    #[test]
126    fn new_label_manager_is_empty() {
127        let mgr = LabelManager::new();
128        assert!(mgr.is_empty());
129        assert_eq!(mgr.len(), 0);
130    }
131
132    #[test]
133    fn default_is_empty() {
134        let mgr = LabelManager::default();
135        assert!(mgr.is_empty());
136    }
137
138    #[test]
139    fn add_label_and_contains() {
140        let secp = Secp256k1::new();
141        let (scan_sk, spend_pk) = test_keys();
142
143        let mut mgr = LabelManager::new();
144        mgr.add_label(&scan_sk, &spend_pk, 1, &secp).unwrap();
145
146        assert!(mgr.contains(1));
147        assert!(!mgr.contains(0));
148        assert!(!mgr.contains(2));
149        assert_eq!(mgr.len(), 1);
150    }
151
152    #[test]
153    fn add_multiple_labels() {
154        let secp = Secp256k1::new();
155        let (scan_sk, spend_pk) = test_keys();
156
157        let mut mgr = LabelManager::new();
158        mgr.add_label(&scan_sk, &spend_pk, 0, &secp).unwrap();
159        mgr.add_label(&scan_sk, &spend_pk, 1, &secp).unwrap();
160        mgr.add_label(&scan_sk, &spend_pk, 42, &secp).unwrap();
161
162        assert!(mgr.contains(0));
163        assert!(mgr.contains(1));
164        assert!(mgr.contains(42));
165        assert!(!mgr.contains(99));
166        assert_eq!(mgr.len(), 3);
167    }
168
169    #[test]
170    fn to_btree_produces_matching_structure() {
171        let secp = Secp256k1::new();
172        let (scan_sk, spend_pk) = test_keys();
173
174        let mut mgr = LabelManager::new();
175        mgr.add_label(&scan_sk, &spend_pk, 1, &secp).unwrap();
176        mgr.add_label(&scan_sk, &spend_pk, 5, &secp).unwrap();
177
178        let btree = mgr.to_btree();
179        assert_eq!(btree.len(), 2);
180
181        // Every entry in the btree should correspond to an entry in our HashMap
182        for (pk, (scalar, m)) in &btree {
183            assert!(mgr.contains(*m));
184            // Verify the label pubkey is label_scalar * G
185            let expected_sk = SecretKey::from_slice(&scalar.to_be_bytes()).expect("valid scalar");
186            let expected_pk = expected_sk.public_key(&secp);
187            assert_eq!(pk, &expected_pk);
188        }
189    }
190
191    #[test]
192    fn different_labels_produce_different_keys() {
193        let secp = Secp256k1::new();
194        let (scan_sk, spend_pk) = test_keys();
195
196        let mut mgr = LabelManager::new();
197        mgr.add_label(&scan_sk, &spend_pk, 0, &secp).unwrap();
198        mgr.add_label(&scan_sk, &spend_pk, 1, &secp).unwrap();
199
200        let btree = mgr.to_btree();
201        let keys: Vec<&PublicKey> = btree.keys().collect();
202        assert_ne!(
203            keys[0], keys[1],
204            "different labels must produce different keys"
205        );
206    }
207}