Skip to main content

cdk_spilman/params/
blinding.rs

1use super::*;
2
3impl ChannelParameters {
4    /// Derive a blinding scalar for P2BK
5    ///
6    /// The `context` parameter specifies which blinded key to derive:
7    /// - "sender_stage1" / "receiver_stage1" - for funding token 2-of-2
8    /// - "sender_stage1_refund" - for funding token expiry refund
9    ///
10    /// Computes: SHA256("Cashu_Spilman_P2BK_v1" || channel_secret || "{channel_id}|{context}|{retry_counter}")
11    /// Retries with incrementing retry_counter until a valid scalar in [1, n-1] is found.
12    ///
13    /// Note: This produces a SHARED blinding scalar for all proofs with the same context.
14    /// For per-proof blinding (stage2), use `stage2_tweak_info_for_role()` instead.
15    fn derive_blinding_scalar(&self, context: &str) -> anyhow::Result<Scalar> {
16        let channel_id = self.get_channel_id();
17
18        for retry_counter in 0u8..=255 {
19            let text = format!("{}|{}|{}", channel_id, context, retry_counter);
20            let mut input = Vec::new();
21            input.extend_from_slice(b"Cashu_Spilman_P2BK_v1");
22            input.extend_from_slice(&self.channel_secret);
23            input.extend_from_slice(text.as_bytes());
24
25            let hash = sha256::Hash::hash(&input);
26            let bytes: [u8; 32] = hash.to_byte_array();
27
28            // Try to create a valid scalar (must be in range [1, n-1])
29            if let Ok(scalar) = Scalar::from_be_bytes(bytes) {
30                // Scalar::from_be_bytes rejects values >= n, and we also reject zero
31                if scalar != Scalar::ZERO {
32                    return Ok(scalar);
33                }
34            }
35        }
36
37        anyhow::bail!("Failed to derive valid blinding scalar after 256 attempts")
38    }
39
40    /// Derive stage 2 P2BK tweak info for a specific output
41    ///
42    /// Uses the per-output ephemeral secret to compute a NUT-28 shared-secret tweak
43    /// alongside the deterministic ephemeral key material for later metadata use.
44    pub(crate) fn stage2_tweak_info_for_role(
45        &self,
46        role: Stage2Role,
47        amount: u64,
48        index: usize,
49    ) -> anyhow::Result<Stage2P2bkTweakInfo> {
50        let role_pubkey = role.pubkey(self);
51        let ephemeral_secret = self.derive_stage2_p2bk_ephemeral_secret_for_output(
52            role.stage2_context(),
53            amount,
54            index,
55        )?;
56        let ephemeral_pubkey = ephemeral_secret.public_key();
57        let ephemeral_shared_secret_x =
58            Self::derive_nut28_shared_secret_x(role_pubkey, &ephemeral_secret)?;
59        let stage2_tweak_scalar =
60            Self::derive__nut28_P2KB_shared_secret_scalar(&ephemeral_shared_secret_x, 0x00)?;
61
62        Ok(Stage2P2bkTweakInfo {
63            ephemeral_secret,
64            ephemeral_pubkey,
65            ephemeral_shared_secret_x,
66            stage2_tweak_scalar,
67        })
68    }
69
70    /// Derive a per-output ephemeral secret for stage 2 contexts
71    ///
72    /// Computes: SHA256("Cashu_Spilman_P2BK_ephemeral_v1" || channel_secret || "{channel_id}|{context}|{amount}|{index}|{retry_counter}")
73    /// Retries with incrementing retry_counter until a valid secret key is found.
74    fn derive_stage2_p2bk_ephemeral_secret_for_output(
75        &self,
76        context: &str,
77        amount: u64,
78        index: usize,
79    ) -> anyhow::Result<SecretKey> {
80        let channel_id = self.get_channel_id();
81
82        for retry_counter in 0u8..=255 {
83            let text = format!(
84                "{}|{}|{}|{}|{}",
85                channel_id, context, amount, index, retry_counter
86            );
87            let mut input = Vec::new();
88            input.extend_from_slice(b"Cashu_Spilman_P2BK_ephemeral_v1");
89            input.extend_from_slice(&self.channel_secret);
90            input.extend_from_slice(text.as_bytes());
91
92            let hash = sha256::Hash::hash(&input);
93            let bytes: [u8; 32] = hash.to_byte_array();
94
95            if let Ok(secret) = SecretKey::from_slice(&bytes) {
96                return Ok(secret);
97            }
98        }
99
100        anyhow::bail!("Failed to derive valid ephemeral secret for output after 256 attempts")
101    }
102
103    /// Derive the raw x-coordinate used by NUT-28 before the KDF step.
104    pub(super) fn derive_nut28_shared_secret_x(
105        pubkey: &cashu::nuts::PublicKey,
106        secret: &SecretKey,
107    ) -> anyhow::Result<[u8; 32]> {
108        let shared_point = pubkey.mul_tweak(&SECP256K1, &secret.as_scalar())?;
109        Ok(shared_point.x_only_public_key().0.serialize())
110    }
111
112    /// Derive NUT-28 P2BK scalar from ephemeral shared secret x-coordinate.
113    ///
114    /// Spec: https://raw.githubusercontent.com/cashubtc/nuts/refs/heads/main/28.md
115    #[allow(non_snake_case)]
116    fn derive__nut28_P2KB_shared_secret_scalar(
117        zx: &[u8; 32],
118        i_byte: u8,
119    ) -> anyhow::Result<Scalar> {
120        let mut input = Vec::new();
121        input.extend_from_slice(b"Cashu_P2BK_v1");
122        input.extend_from_slice(zx);
123        input.push(i_byte);
124
125        let hash = sha256::Hash::hash(&input);
126        let bytes: [u8; 32] = hash.to_byte_array();
127        if let Ok(scalar) = Scalar::from_be_bytes(bytes) {
128            if scalar != Scalar::ZERO {
129                return Ok(scalar);
130            }
131        }
132
133        input.push(0xff);
134        let hash = sha256::Hash::hash(&input);
135        let bytes: [u8; 32] = hash.to_byte_array();
136        if let Ok(scalar) = Scalar::from_be_bytes(bytes) {
137            if scalar != Scalar::ZERO {
138                return Ok(scalar);
139            }
140        }
141
142        anyhow::bail!("Failed to derive valid P2BK scalar")
143    }
144
145    /// Get the blinded sender (Alice) pubkey for stage 1 P2BK
146    ///
147    /// Computes the blinded pubkey that corresponds to Alice's blinded secret key.
148    /// This handles BIP-340 parity: if Alice's pubkey has odd Y, we negate it first.
149    ///
150    /// The formula matches `derive_blinded_secret_key`:
151    /// - If even Y: P' = P + r*G (matches k = p + r)
152    /// - If odd Y:  P' = -P + r*G (matches k = -p + r)
153    pub fn get_sender_blinded_pubkey_for_stage1(&self) -> anyhow::Result<cashu::nuts::PublicKey> {
154        let r = self.derive_blinding_scalar("sender_stage1")?;
155        derive_blinded_pubkey(&self.sender_pubkey, &r)
156    }
157
158    /// Get the blinded receiver (Charlie) pubkey for stage 1 P2BK
159    ///
160    /// Computes the blinded pubkey that corresponds to Charlie's blinded secret key.
161    /// This handles BIP-340 parity: if Charlie's pubkey has odd Y, we negate it first.
162    ///
163    /// The formula matches `derive_blinded_secret_key`:
164    /// - If even Y: P' = P + r*G (matches k = p + r)
165    /// - If odd Y:  P' = -P + r*G (matches k = -p + r)
166    pub fn get_receiver_blinded_pubkey_for_stage1(&self) -> anyhow::Result<cashu::nuts::PublicKey> {
167        let r = self.derive_blinding_scalar("receiver_stage1")?;
168        derive_blinded_pubkey(&self.receiver_pubkey, &r)
169    }
170
171    /// Derive the blinded sender secret key for stage 1 signing
172    ///
173    /// For P2BK, Alice must sign with a blinded private key k such that k*G = P'.
174    /// This handles BIP-340 parity: if Alice's pubkey has odd Y, we negate her
175    /// private key before adding the blinding scalar.
176    pub fn get_sender_blinded_secret_key_for_stage1(
177        &self,
178        alice_secret: &SecretKey,
179    ) -> anyhow::Result<SecretKey> {
180        let r = self.derive_blinding_scalar("sender_stage1")?;
181        derive_blinded_secret_key(alice_secret, &r)
182    }
183
184    /// Get the sender's P2BK blinding scalar for stage 1 signing.
185    ///
186    /// This is the tweak scalar that must be added to Alice's secret key
187    /// (with BIP-340 parity handling) to produce the blinded signing key.
188    /// Used by the external signer flow in SpilmanClientBridge.
189    pub fn derive_sender_blinding_scalar_for_stage1(&self) -> anyhow::Result<Scalar> {
190        self.derive_blinding_scalar("sender_stage1")
191    }
192
193    /// Get the receiver's P2BK blinding scalar for stage 1 signing.
194    ///
195    /// This is the tweak scalar that must be added to Charlie's secret key
196    /// (with BIP-340 parity handling) to produce the blinded signing key.
197    /// Used by the external signer flow in SpilmanBridge.
198    pub fn derive_receiver_blinding_scalar_for_stage1(&self) -> anyhow::Result<Scalar> {
199        self.derive_blinding_scalar("receiver_stage1")
200    }
201
202    /// Get the blinded sender (Alice) pubkey for stage 1 expiry refund
203    ///
204    /// Uses a DIFFERENT blinding tweak than the 2-of-2 spending path, so the mint
205    /// cannot correlate Alice's refund to the normal channel close.
206    pub fn get_sender_blinded_pubkey_for_stage1_refund(
207        &self,
208    ) -> anyhow::Result<cashu::nuts::PublicKey> {
209        let r = self.derive_blinding_scalar("sender_stage1_refund")?;
210        derive_blinded_pubkey(&self.sender_pubkey, &r)
211    }
212
213    /// Derive the blinded sender secret key for stage 1 expiry refund
214    ///
215    /// Uses a DIFFERENT blinding tweak than the 2-of-2 spending path.
216    /// Alice uses this to sign when reclaiming funds after expiry.
217    pub fn get_sender_blinded_secret_key_for_stage1_refund(
218        &self,
219        alice_secret: &SecretKey,
220    ) -> anyhow::Result<SecretKey> {
221        let r = self.derive_blinding_scalar("sender_stage1_refund")?;
222        derive_blinded_secret_key(alice_secret, &r)
223    }
224
225    /// Derive the blinded receiver secret key for stage 1 signing
226    ///
227    /// For P2BK, Charlie must sign with a blinded private key k such that k*G = P'.
228    /// This handles BIP-340 parity: if Charlie's pubkey has odd Y, we negate his
229    /// private key before adding the blinding scalar.
230    pub fn get_receiver_blinded_secret_key_for_stage1(
231        &self,
232        charlie_secret: &SecretKey,
233    ) -> anyhow::Result<SecretKey> {
234        let r = self.derive_blinding_scalar("receiver_stage1")?;
235        derive_blinded_secret_key(charlie_secret, &r)
236    }
237
238    /// Get the blinded sender (Alice) pubkey for a specific stage 2 output
239    ///
240    /// Used for stage 1 outputs - each of Alice's proofs is locked to a UNIQUE
241    /// blinded pubkey derived from (amount, index). She'll need to sign with
242    /// the corresponding secret key in stage 2.
243    ///
244    /// This provides better privacy than a shared pubkey - the mint cannot
245    /// trivially link proofs from the same channel closure.
246    pub fn get_sender_blinded_pubkey_for_stage2_output(
247        &self,
248        amount: u64,
249        index: usize,
250    ) -> anyhow::Result<cashu::nuts::PublicKey> {
251        let tweak_info = self.stage2_tweak_info_for_role(Stage2Role::Sender, amount, index)?;
252        derive_blinded_pubkey(&self.sender_pubkey, &tweak_info.stage2_tweak_scalar)
253    }
254
255    /// Get the blinded receiver (Charlie) pubkey for a specific stage 2 output
256    ///
257    /// Used for stage 1 outputs - each of Charlie's proofs is locked to a UNIQUE
258    /// blinded pubkey derived from (amount, index). He'll need to sign with
259    /// the corresponding secret key in stage 2.
260    ///
261    /// This provides better privacy than a shared pubkey - the mint cannot
262    /// trivially link proofs from the same channel closure.
263    pub fn get_receiver_blinded_pubkey_for_stage2_output(
264        &self,
265        amount: u64,
266        index: usize,
267    ) -> anyhow::Result<cashu::nuts::PublicKey> {
268        let tweak_info = self.stage2_tweak_info_for_role(Stage2Role::Receiver, amount, index)?;
269        derive_blinded_pubkey(&self.receiver_pubkey, &tweak_info.stage2_tweak_scalar)
270    }
271
272    /// Derive the blinded sender secret key for a specific stage 2 output
273    ///
274    /// Alice uses this to sign when spending a specific stage 1 proof in stage 2.
275    /// Each proof has a unique blinded secret key derived from (amount, index).
276    pub fn get_sender_blinded_secret_key_for_stage2_output(
277        &self,
278        alice_secret: &SecretKey,
279        amount: u64,
280        index: usize,
281    ) -> anyhow::Result<SecretKey> {
282        let tweak_info = self.stage2_tweak_info_for_role(Stage2Role::Sender, amount, index)?;
283        derive_blinded_secret_key(alice_secret, &tweak_info.stage2_tweak_scalar)
284    }
285
286    /// Derive the blinded receiver secret key for a specific stage 2 output
287    ///
288    /// Charlie uses this to sign when spending a specific stage 1 proof in stage 2.
289    /// Each proof has a unique blinded secret key derived from (amount, index).
290    pub fn get_receiver_blinded_secret_key_for_stage2_output(
291        &self,
292        charlie_secret: &SecretKey,
293        amount: u64,
294        index: usize,
295    ) -> anyhow::Result<SecretKey> {
296        let tweak_info = self.stage2_tweak_info_for_role(Stage2Role::Receiver, amount, index)?;
297        derive_blinded_secret_key(charlie_secret, &tweak_info.stage2_tweak_scalar)
298    }
299
300    /// Get a string representation of the unit
301    pub fn unit_name(&self) -> &str {
302        match self.unit {
303            CurrencyUnit::Sat => "sat",
304            CurrencyUnit::Msat => "msat",
305            CurrencyUnit::Usd => "usd",
306            CurrencyUnit::Eur => "eur",
307            _ => "units",
308        }
309    }
310
311    /// Get the STAGE2 blinded pubkey for a stage 1 output context ("sender" or "receiver")
312    ///
313    /// Returns the stage2 blinded pubkey for use in stage 1 commitment outputs:
314    /// - "receiver" → Charlie's per-proof blinded pubkey (stage2 context)
315    /// - "sender" → Alice's per-proof blinded pubkey (stage2 context)
316    /// - "funding" → error (funding uses 2-of-2 with stage1 blinded pubkeys)
317    ///
318    /// Uses "stage2" blinding context because these are the keys needed to sign in stage 2.
319    /// Each proof gets a UNIQUE blinded pubkey derived from (amount, index) for better privacy.
320    pub fn get_stage2_blinded_pubkey_for_stage1_output(
321        &self,
322        context: &str,
323        amount: u64,
324        index: usize,
325    ) -> Result<cashu::nuts::PublicKey, anyhow::Error> {
326        match context {
327            "receiver" => self.get_receiver_blinded_pubkey_for_stage2_output(amount, index),
328            "sender" => self.get_sender_blinded_pubkey_for_stage2_output(amount, index),
329            "funding" => anyhow::bail!(
330                "Funding context requires 2-of-2 blinded pubkeys, use new_funding() instead"
331            ),
332            _ => anyhow::bail!("Unknown context: {}", context),
333        }
334    }
335
336    pub(crate) fn stage2_p2pk_e_for_role(
337        &self,
338        role: Stage2Role,
339        amount: u64,
340        index: usize,
341    ) -> Result<cashu::nuts::PublicKey, anyhow::Error> {
342        let tweak_info = self.stage2_tweak_info_for_role(role, amount, index)?;
343
344        Ok(tweak_info.ephemeral_pubkey)
345    }
346
347    pub(crate) fn attach_stage2_p2pk_e(
348        &self,
349        proof: &mut cashu::nuts::Proof,
350        role: Stage2Role,
351        amount: u64,
352        index: usize,
353    ) -> Result<(), anyhow::Error> {
354        proof.p2pk_e = Some(self.stage2_p2pk_e_for_role(role, amount, index)?);
355        Ok(())
356    }
357
358    /// Create a deterministic output with blinding using the channel ID and channel secret
359    /// Uses channel_secret, channel_id, context, amount, and index in the derivation per NUT-XX spec
360    ///
361    /// The context parameter specifies the role: "sender", "receiver", or "funding"
362    /// - "sender"/"receiver" create simple P2PK outputs for commitments using stage2 blinded pubkeys
363    /// - "funding" creates P2PK outputs with 2-of-2 multisig + expiry conditions
364    pub fn create_deterministic_output_with_blinding(
365        &self,
366        context: &str,
367        amount: u64,
368        index: usize,
369    ) -> Result<DeterministicSecretWithBlinding, anyhow::Error> {
370        let channel_id = self.get_channel_id();
371
372        // Derive deterministic nonce: SHA256(channel_secret || "{channel_id}|{context}|{amount}|nonce|{index}")
373        let nonce_text = format!("{}|{}|{}|nonce|{}", channel_id, context, amount, index);
374        let mut nonce_input = Vec::new();
375        nonce_input.extend_from_slice(&self.channel_secret);
376        nonce_input.extend_from_slice(nonce_text.as_bytes());
377
378        let hash = sha256::Hash::hash(&nonce_input);
379        let nonce = hex::encode(hash.to_byte_array());
380
381        // Derive deterministic blinding factor: SHA256(channel_secret || "{channel_id}|{context}|{amount}|blinding|{index}")
382        let blinding_text = format!("{}|{}|{}|blinding|{}", channel_id, context, amount, index);
383        let mut blinding_input = Vec::new();
384        blinding_input.extend_from_slice(&self.channel_secret);
385        blinding_input.extend_from_slice(blinding_text.as_bytes());
386
387        let hash = sha256::Hash::hash(&blinding_input);
388        let blinding_factor = SecretKey::from_slice(hash.as_byte_array())?;
389
390        // Handle funding context separately (requires 2-of-2 blinded pubkeys + expiry)
391        if context == "funding" {
392            DeterministicSecretWithBlinding::new_funding(
393                self,
394                nonce,
395                blinding_factor,
396                amount,
397                index,
398            )
399        } else {
400            // For sender/receiver contexts, create simple P2PK outputs with BLINDED pubkeys
401            // Each proof gets a UNIQUE blinded pubkey derived from (amount, index)
402            let pubkey =
403                self.get_stage2_blinded_pubkey_for_stage1_output(context, amount, index)?;
404            DeterministicSecretWithBlinding::new_p2pk(
405                &pubkey,
406                nonce,
407                blinding_factor,
408                amount,
409                index,
410            )
411        }
412    }
413
414    /// Get the minimum funding token amount for a given capacity using double inverse
415    ///
416    /// This computes the minimum funding_token_amount needed to achieve at least
417    /// the specified capacity after both fee stages, using the given keyset.
418    ///
419    /// Applies the inverse fee calculation twice to the capacity:
420    /// 1. capacity → post-stage-1 nominal (accounting for stage 2 fees)
421    /// 2. post-stage-1 nominal → funding token nominal (accounting for stage 1 fees)
422    pub fn get_minimum_funding_token_amount(
423        capacity: u64,
424        keyset_info: &KeysetInfo,
425        maximum_amount_for_one_output: u64,
426    ) -> anyhow::Result<u64> {
427        let max_amt = maximum_amount_for_one_output;
428
429        // First inverse: capacity → post-stage-1 nominal (accounting for stage 2 fees)
430        let first_inverse =
431            keyset_info.inverse_deterministic_value_after_fees(capacity, max_amt)?;
432        let post_stage1_nominal = first_inverse.nominal_value;
433
434        // Second inverse: post-stage-1 nominal → funding token nominal (accounting for stage 1 fees)
435        let second_inverse =
436            keyset_info.inverse_deterministic_value_after_fees(post_stage1_nominal, max_amt)?;
437        let funding_token_nominal = second_inverse.nominal_value;
438
439        Ok(funding_token_nominal)
440    }
441
442    /// Get the total funding token amount
443    ///
444    /// Returns the explicit funding_token_amount field.
445    pub fn get_total_funding_token_amount(&self) -> anyhow::Result<u64> {
446        Ok(self.funding_token_amount)
447    }
448
449    /// Get the value available after stage 1 fees with a specific keyset
450    pub fn get_value_after_stage1_with_keyset(
451        &self,
452        keyset_info: &KeysetInfo,
453    ) -> anyhow::Result<u64> {
454        // Apply forward to get actual value after stage 1 fees (spending the funding token)
455        // using the provided keyset for the outputs
456        let value_after_stage1 = keyset_info.deterministic_value_after_fees(
457            self.funding_token_amount,
458            self.maximum_amount_for_one_output,
459        )?;
460
461        Ok(value_after_stage1)
462    }
463
464    /// Get the value available after stage 1 fees
465    ///
466    /// Takes the funding token amount and applies the forward fee calculation
467    /// to determine the actual amount available after the swap transaction (stage 1).
468    ///
469    /// This represents the total amount that will be distributed between Alice and Charlie
470    /// in the commitment transaction outputs.
471    ///
472    /// Returns the actual value after stage 1 fees
473    pub fn get_value_after_stage1(&self) -> anyhow::Result<u64> {
474        self.get_value_after_stage1_with_keyset(&self.keyset_info)
475    }
476
477    /// Compute the actual de facto balance from an intended balance
478    ///
479    /// Due to output denomination constraints and fee rounding, the actual balance
480    /// that can be created may differ slightly from the intended balance.
481    ///
482    /// This method:
483    /// 1. Applies inverse to find the nominal value needed for the intended balance
484    /// 2. Applies deterministic_value to that nominal to get the actual de facto balance
485    ///
486    /// Returns the actual balance that will be created
487    pub fn get_de_facto_balance(&self, intended_balance: u64) -> anyhow::Result<u64> {
488        let max_amt = self.maximum_amount_for_one_output;
489
490        // Apply inverse to get nominal value needed
491        let inverse_result = self
492            .keyset_info
493            .inverse_deterministic_value_after_fees(intended_balance, max_amt)?;
494        let nominal_value = inverse_result.nominal_value;
495
496        // Apply deterministic_value to get actual balance
497        let actual_balance = self
498            .keyset_info
499            .deterministic_value_after_fees(nominal_value, max_amt)?;
500
501        Ok(actual_balance)
502    }
503}