1pub type ChannelId = String;
7
8use serde::{Deserialize, Serialize};
9
10use bitcoin::hashes::{sha256, Hash};
11use bitcoin::secp256k1::ecdh::SharedSecret;
12use bitcoin::secp256k1::{Parity, Scalar};
13use cashu::nuts::{CurrencyUnit, SecretKey};
14#[cfg(test)]
15use cashu::nuts::{Id, Keys, PublicKey};
16use cashu::util::hex;
17#[cfg(test)]
18use cashu::Amount;
19use cashu::SECP256K1;
20#[cfg(test)]
21use std::collections::BTreeMap;
22#[cfg(test)]
23use std::str::FromStr;
24
25use super::deterministic::DeterministicSecretWithBlinding;
26use super::keysets_and_amounts::KeysetInfo;
27
28pub(crate) struct Stage2P2bkTweakInfo {
29 #[allow(dead_code)]
30 pub(crate) ephemeral_secret: SecretKey,
31 #[allow(dead_code)]
32 pub(crate) ephemeral_pubkey: cashu::nuts::PublicKey,
33 #[allow(dead_code)]
34 pub(crate) ephemeral_shared_secret_x: [u8; 32],
35 #[allow(dead_code)]
36 pub(crate) stage2_tweak_scalar: Scalar,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub(crate) enum Stage2Role {
41 Sender,
42 Receiver,
43}
44
45impl Stage2Role {
46 fn stage2_context(self) -> &'static str {
47 match self {
48 Self::Sender => "sender_stage2",
49 Self::Receiver => "receiver_stage2",
50 }
51 }
52
53 fn pubkey(self, params: &ChannelParameters) -> &cashu::nuts::PublicKey {
54 match self {
55 Self::Sender => ¶ms.sender_pubkey,
56 Self::Receiver => ¶ms.receiver_pubkey,
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ChannelParameters {
64 pub sender_pubkey: cashu::nuts::PublicKey,
66 pub receiver_pubkey: cashu::nuts::PublicKey,
68 pub mint: String,
70 pub unit: CurrencyUnit,
72 pub capacity: u64,
74 pub funding_token_amount: u64,
76 pub expiry_timestamp: u64,
78 pub setup_timestamp: u64,
80 pub keyset_info: KeysetInfo,
82 pub maximum_amount_for_one_output: u64,
84 pub channel_secret: [u8; 32],
86}
87
88pub fn compute_channel_secret(
95 my_secret: &cashu::nuts::SecretKey,
96 their_pubkey: &cashu::nuts::PublicKey,
97) -> [u8; 32] {
98 let raw_ecdh = SharedSecret::new(their_pubkey, my_secret).secret_bytes();
99 let mut input = Vec::new();
100 input.extend_from_slice(b"Cashu_Spilman_channel_secret_v1");
101 input.extend_from_slice(&raw_ecdh);
102 sha256::Hash::hash(&input).to_byte_array()
103}
104
105#[cfg(test)]
107pub(crate) fn mock_keyset_info(amounts: Vec<u64>, input_fee_ppk: u64) -> KeysetInfo {
108 let mut keys_map = BTreeMap::new();
109 let dummy_pubkey =
110 PublicKey::from_str("02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2")
111 .unwrap();
112 for &amt in &amounts {
113 keys_map.insert(Amount::from(amt), dummy_pubkey);
114 }
115
116 let mut amounts_largest_first = amounts;
117 amounts_largest_first.sort_by(|a, b| b.cmp(a));
118
119 let active_keys = Keys::new(keys_map);
120 let keyset_id = Id::v1_from_keys(&active_keys);
121
122 KeysetInfo::new(
123 keyset_id,
124 CurrencyUnit::Sat,
125 active_keys,
126 input_fee_ppk,
127 None,
128 )
129}
130
131fn derive_blinded_secret_key(secret: &SecretKey, r: &Scalar) -> anyhow::Result<SecretKey> {
138 let pubkey = secret.public_key();
142 let inner_pubkey: &bitcoin::secp256k1::PublicKey = &pubkey;
143 let (_, parity) = inner_pubkey.x_only_public_key();
144
145 let inner_secret: bitcoin::secp256k1::SecretKey = **secret;
148
149 let effective_secret = if parity == Parity::Odd {
152 inner_secret.negate()
153 } else {
154 inner_secret
155 };
156
157 let blinded = effective_secret
159 .add_tweak(r)
160 .map_err(|e| anyhow::anyhow!("Failed to add blinding tweak: {}", e))?;
161
162 Ok(blinded.into())
163}
164
165fn derive_blinded_pubkey(
176 pubkey: &cashu::nuts::PublicKey,
177 r: &Scalar,
178) -> anyhow::Result<cashu::nuts::PublicKey> {
179 let inner_pubkey: &bitcoin::secp256k1::PublicKey = pubkey;
181 let (_, parity) = inner_pubkey.x_only_public_key();
182
183 let effective_pubkey = if parity == Parity::Odd {
186 inner_pubkey.negate(&SECP256K1)
187 } else {
188 *inner_pubkey
189 };
190
191 let blinded = effective_pubkey
193 .add_exp_tweak(&SECP256K1, r)
194 .map_err(|e| anyhow::anyhow!("Failed to blind pubkey: {}", e))?;
195
196 Ok(blinded.into())
197}
198
199impl ChannelParameters {
200 #[allow(clippy::too_many_arguments)]
202 pub fn new(
203 sender_pubkey: cashu::nuts::PublicKey,
204 receiver_pubkey: cashu::nuts::PublicKey,
205 mint: String,
206 unit: CurrencyUnit,
207 capacity: u64,
208 funding_token_amount: u64,
209 expiry_timestamp: u64,
210 setup_timestamp: u64,
211 keyset_info: KeysetInfo,
212 maximum_amount_for_one_output: u64,
213 channel_secret: [u8; 32],
214 ) -> anyhow::Result<Self> {
215 if keyset_info.input_fee_ppk > 999 {
217 anyhow::bail!(
218 "input_fee_ppk must be between 0 and 999 (inclusive), got {}",
219 keyset_info.input_fee_ppk
220 );
221 }
222
223 let max_capacity = {
225 let after_stage1 = keyset_info.deterministic_value_after_fees(
226 funding_token_amount,
227 maximum_amount_for_one_output,
228 )?;
229 keyset_info
230 .deterministic_value_after_fees(after_stage1, maximum_amount_for_one_output)?
231 };
232 if capacity > max_capacity {
233 anyhow::bail!(
234 "capacity {} exceeds maximum achievable capacity {} for funding_token_amount {} \
235 (capacity must be <= forward(forward(funding_token_amount)))",
236 capacity,
237 max_capacity,
238 funding_token_amount
239 );
240 }
241
242 Ok(Self {
243 sender_pubkey,
244 receiver_pubkey,
245 mint,
246 unit,
247 capacity,
248 funding_token_amount,
249 expiry_timestamp,
250 setup_timestamp,
251 keyset_info,
252 maximum_amount_for_one_output,
253 channel_secret,
254 })
255 }
256
257 #[allow(clippy::too_many_arguments)]
270 pub fn new_with_secret_key(
271 sender_pubkey: cashu::nuts::PublicKey,
272 receiver_pubkey: cashu::nuts::PublicKey,
273 mint: String,
274 unit: CurrencyUnit,
275 capacity: u64,
276 funding_token_amount: u64,
277 expiry_timestamp: u64,
278 setup_timestamp: u64,
279 keyset_info: KeysetInfo,
280 maximum_amount_for_one_output: u64,
281 my_secret: &SecretKey,
282 ) -> anyhow::Result<Self> {
283 let my_pubkey = my_secret.public_key();
284
285 let their_pubkey = if my_pubkey == sender_pubkey {
287 &receiver_pubkey
289 } else if my_pubkey == receiver_pubkey {
290 &sender_pubkey
292 } else {
293 anyhow::bail!(
294 "Secret key's public key doesn't match either sender_pubkey or receiver_pubkey"
295 );
296 };
297
298 let channel_secret = compute_channel_secret(my_secret, their_pubkey);
300
301 Self::new(
302 sender_pubkey,
303 receiver_pubkey,
304 mint,
305 unit,
306 capacity,
307 funding_token_amount,
308 expiry_timestamp,
309 setup_timestamp,
310 keyset_info,
311 maximum_amount_for_one_output,
312 channel_secret,
313 )
314 }
315
316 pub fn from_json_with_secret_key(
326 json_str: &str,
327 keyset_info: KeysetInfo,
328 my_secret: &SecretKey,
329 ) -> anyhow::Result<Self> {
330 let json: serde_json::Value =
332 serde_json::from_str(json_str).map_err(|e| anyhow::anyhow!("Invalid JSON: {}", e))?;
333
334 let sender_pubkey_hex = json["sender_pubkey"]
335 .as_str()
336 .ok_or_else(|| anyhow::anyhow!("Missing or invalid 'sender_pubkey' field"))?;
337 let sender_pubkey: cashu::nuts::PublicKey = sender_pubkey_hex
338 .parse()
339 .map_err(|e| anyhow::anyhow!("Invalid sender_pubkey: {}", e))?;
340
341 let receiver_pubkey_hex = json["receiver_pubkey"]
342 .as_str()
343 .ok_or_else(|| anyhow::anyhow!("Missing or invalid 'receiver_pubkey' field"))?;
344 let receiver_pubkey: cashu::nuts::PublicKey = receiver_pubkey_hex
345 .parse()
346 .map_err(|e| anyhow::anyhow!("Invalid receiver_pubkey: {}", e))?;
347
348 let my_pubkey = my_secret.public_key();
350 let their_pubkey = if my_pubkey == sender_pubkey {
351 &receiver_pubkey
352 } else if my_pubkey == receiver_pubkey {
353 &sender_pubkey
354 } else {
355 anyhow::bail!(
356 "Secret key's public key doesn't match either sender_pubkey or receiver_pubkey"
357 );
358 };
359
360 let channel_secret = compute_channel_secret(my_secret, their_pubkey);
361
362 Self::from_json_with_channel_secret(json_str, keyset_info, channel_secret)
363 }
364
365 pub fn from_json_with_channel_secret(
369 json_str: &str,
370 keyset_info: KeysetInfo,
371 channel_secret: [u8; 32],
372 ) -> anyhow::Result<Self> {
373 let json: serde_json::Value =
374 serde_json::from_str(json_str).map_err(|e| anyhow::anyhow!("Invalid JSON: {}", e))?;
375
376 let keyset_id_str = json["keyset_id"]
378 .as_str()
379 .or_else(|| json["keysetId"].as_str())
380 .ok_or_else(|| anyhow::anyhow!("Missing or invalid 'keyset_id' field"))?;
381 let json_keyset_id: cashu::nuts::Id = keyset_id_str
382 .parse()
383 .map_err(|e| anyhow::anyhow!("Invalid keyset_id: {}", e))?;
384
385 let json_input_fee_ppk = json["input_fee_ppk"]
386 .as_u64()
387 .or_else(|| json["inputFeePpk"].as_u64())
388 .ok_or_else(|| anyhow::anyhow!("Missing or invalid 'input_fee_ppk' field"))?;
389
390 if keyset_info.keyset_id != json_keyset_id {
392 anyhow::bail!(
393 "keyset_id mismatch: JSON has {}, KeysetInfo has {}",
394 json_keyset_id,
395 keyset_info.keyset_id
396 );
397 }
398 if keyset_info.input_fee_ppk != json_input_fee_ppk {
399 anyhow::bail!(
400 "input_fee_ppk mismatch: JSON has {}, KeysetInfo has {}",
401 json_input_fee_ppk,
402 keyset_info.input_fee_ppk
403 );
404 }
405
406 let mint = json["mint"]
408 .as_str()
409 .ok_or_else(|| anyhow::anyhow!("Missing or invalid 'mint' field"))?
410 .to_string();
411
412 let unit_str = json["unit"]
413 .as_str()
414 .ok_or_else(|| anyhow::anyhow!("Missing or invalid 'unit' field"))?;
415 let unit = match unit_str {
416 "sat" => CurrencyUnit::Sat,
417 "msat" => CurrencyUnit::Msat,
418 "usd" => CurrencyUnit::Usd,
419 "eur" => CurrencyUnit::Eur,
420 _ => anyhow::bail!("Unknown unit: {}", unit_str),
421 };
422
423 let capacity = json["capacity"]
424 .as_u64()
425 .ok_or_else(|| anyhow::anyhow!("Missing or invalid 'capacity' field"))?;
426
427 let funding_token_amount = json["funding_token_amount"]
428 .as_u64()
429 .ok_or_else(|| anyhow::anyhow!("Missing or invalid 'funding_token_amount' field"))?;
430
431 let maximum_amount_for_one_output = json["maximum_amount"]
432 .as_u64()
433 .or_else(|| json["maximum_amount_for_one_output"].as_u64())
434 .ok_or_else(|| anyhow::anyhow!("Missing or invalid 'maximum_amount' field"))?;
435
436 let setup_timestamp = json["setup_timestamp"]
437 .as_u64()
438 .ok_or_else(|| anyhow::anyhow!("Missing or invalid 'setup_timestamp' field"))?;
439
440 let sender_pubkey_hex = json["sender_pubkey"]
441 .as_str()
442 .ok_or_else(|| anyhow::anyhow!("Missing or invalid 'sender_pubkey' field"))?;
443 let sender_pubkey: cashu::nuts::PublicKey = sender_pubkey_hex
444 .parse()
445 .map_err(|e| anyhow::anyhow!("Invalid sender_pubkey: {}", e))?;
446
447 let receiver_pubkey_hex = json["receiver_pubkey"]
448 .as_str()
449 .ok_or_else(|| anyhow::anyhow!("Missing or invalid 'receiver_pubkey' field"))?;
450 let receiver_pubkey: cashu::nuts::PublicKey = receiver_pubkey_hex
451 .parse()
452 .map_err(|e| anyhow::anyhow!("Invalid receiver_pubkey: {}", e))?;
453
454 let expiry_timestamp = json["expiry_timestamp"]
455 .as_u64()
456 .ok_or_else(|| anyhow::anyhow!("Missing or invalid 'expiry_timestamp' field"))?;
457
458 Self::new(
459 sender_pubkey,
460 receiver_pubkey,
461 mint,
462 unit,
463 capacity,
464 funding_token_amount,
465 expiry_timestamp,
466 setup_timestamp,
467 keyset_info,
468 maximum_amount_for_one_output,
469 channel_secret,
470 )
471 }
472
473 pub fn get_capacity(&self) -> u64 {
476 self.capacity
477 }
478
479 pub fn get_channel_id_bytes(&self) -> [u8; 32] {
486 let params_string = format!(
487 "{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}",
488 self.mint,
489 self.unit_name(),
490 self.capacity,
491 self.funding_token_amount,
492 self.keyset_info.keyset_id,
493 self.keyset_info.input_fee_ppk,
494 self.maximum_amount_for_one_output,
495 self.setup_timestamp,
496 self.sender_pubkey.to_hex(),
497 self.receiver_pubkey.to_hex(),
498 self.expiry_timestamp,
499 hex::encode(self.channel_secret)
500 );
501 sha256::Hash::hash(params_string.as_bytes()).to_byte_array()
502 }
503
504 pub fn get_channel_id(&self) -> String {
506 hex::encode(self.get_channel_id_bytes())
507 }
508
509 pub fn get_channel_id_params_json(&self) -> String {
512 serde_json::json!({
513 "mint": self.mint,
514 "unit": self.unit_name(),
515 "capacity": self.capacity,
516 "funding_token_amount": self.funding_token_amount,
517 "keyset_id": self.keyset_info.keyset_id.to_string(),
518 "input_fee_ppk": self.keyset_info.input_fee_ppk,
519 "maximum_amount": self.maximum_amount_for_one_output,
520 "setup_timestamp": self.setup_timestamp,
521 "sender_pubkey": self.sender_pubkey.to_hex(),
522 "receiver_pubkey": self.receiver_pubkey.to_hex(),
523 "expiry_timestamp": self.expiry_timestamp
524 })
525 .to_string()
526 }
527}
528
529mod blinding;
530
531#[cfg(test)]
532mod tests;