pub type ChannelId = String;
use serde::{Deserialize, Serialize};
use bitcoin::hashes::{sha256, Hash};
use bitcoin::secp256k1::ecdh::SharedSecret;
use bitcoin::secp256k1::{Parity, Scalar};
use cashu::nuts::{CurrencyUnit, SecretKey};
#[cfg(test)]
use cashu::nuts::{Id, Keys, PublicKey};
use cashu::util::hex;
#[cfg(test)]
use cashu::Amount;
use cashu::SECP256K1;
#[cfg(test)]
use std::collections::BTreeMap;
#[cfg(test)]
use std::str::FromStr;
use super::deterministic::DeterministicSecretWithBlinding;
use super::keysets_and_amounts::KeysetInfo;
pub(crate) struct Stage2P2bkTweakInfo {
#[allow(dead_code)]
pub(crate) ephemeral_secret: SecretKey,
#[allow(dead_code)]
pub(crate) ephemeral_pubkey: cashu::nuts::PublicKey,
#[allow(dead_code)]
pub(crate) ephemeral_shared_secret_x: [u8; 32],
#[allow(dead_code)]
pub(crate) stage2_tweak_scalar: Scalar,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Stage2Role {
Sender,
Receiver,
}
impl Stage2Role {
fn stage2_context(self) -> &'static str {
match self {
Self::Sender => "sender_stage2",
Self::Receiver => "receiver_stage2",
}
}
fn pubkey(self, params: &ChannelParameters) -> &cashu::nuts::PublicKey {
match self {
Self::Sender => ¶ms.sender_pubkey,
Self::Receiver => ¶ms.receiver_pubkey,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelParameters {
pub sender_pubkey: cashu::nuts::PublicKey,
pub receiver_pubkey: cashu::nuts::PublicKey,
pub mint: String,
pub unit: CurrencyUnit,
pub capacity: u64,
pub funding_token_amount: u64,
pub expiry_timestamp: u64,
pub setup_timestamp: u64,
pub keyset_info: KeysetInfo,
pub maximum_amount_for_one_output: u64,
pub channel_secret: [u8; 32],
}
pub fn compute_channel_secret(
my_secret: &cashu::nuts::SecretKey,
their_pubkey: &cashu::nuts::PublicKey,
) -> [u8; 32] {
let raw_ecdh = SharedSecret::new(their_pubkey, my_secret).secret_bytes();
let mut input = Vec::new();
input.extend_from_slice(b"Cashu_Spilman_channel_secret_v1");
input.extend_from_slice(&raw_ecdh);
sha256::Hash::hash(&input).to_byte_array()
}
#[cfg(test)]
pub(crate) fn mock_keyset_info(amounts: Vec<u64>, input_fee_ppk: u64) -> KeysetInfo {
let mut keys_map = BTreeMap::new();
let dummy_pubkey =
PublicKey::from_str("02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2")
.unwrap();
for &amt in &amounts {
keys_map.insert(Amount::from(amt), dummy_pubkey);
}
let mut amounts_largest_first = amounts;
amounts_largest_first.sort_by(|a, b| b.cmp(a));
let active_keys = Keys::new(keys_map);
let keyset_id = Id::v1_from_keys(&active_keys);
KeysetInfo::new(
keyset_id,
CurrencyUnit::Sat,
active_keys,
input_fee_ppk,
None,
)
}
fn derive_blinded_secret_key(secret: &SecretKey, r: &Scalar) -> anyhow::Result<SecretKey> {
let pubkey = secret.public_key();
let inner_pubkey: &bitcoin::secp256k1::PublicKey = &pubkey;
let (_, parity) = inner_pubkey.x_only_public_key();
let inner_secret: bitcoin::secp256k1::SecretKey = **secret;
let effective_secret = if parity == Parity::Odd {
inner_secret.negate()
} else {
inner_secret
};
let blinded = effective_secret
.add_tweak(r)
.map_err(|e| anyhow::anyhow!("Failed to add blinding tweak: {}", e))?;
Ok(blinded.into())
}
fn derive_blinded_pubkey(
pubkey: &cashu::nuts::PublicKey,
r: &Scalar,
) -> anyhow::Result<cashu::nuts::PublicKey> {
let inner_pubkey: &bitcoin::secp256k1::PublicKey = pubkey;
let (_, parity) = inner_pubkey.x_only_public_key();
let effective_pubkey = if parity == Parity::Odd {
inner_pubkey.negate(&SECP256K1)
} else {
*inner_pubkey
};
let blinded = effective_pubkey
.add_exp_tweak(&SECP256K1, r)
.map_err(|e| anyhow::anyhow!("Failed to blind pubkey: {}", e))?;
Ok(blinded.into())
}
impl ChannelParameters {
#[allow(clippy::too_many_arguments)]
pub fn new(
sender_pubkey: cashu::nuts::PublicKey,
receiver_pubkey: cashu::nuts::PublicKey,
mint: String,
unit: CurrencyUnit,
capacity: u64,
funding_token_amount: u64,
expiry_timestamp: u64,
setup_timestamp: u64,
keyset_info: KeysetInfo,
maximum_amount_for_one_output: u64,
channel_secret: [u8; 32],
) -> anyhow::Result<Self> {
if keyset_info.input_fee_ppk > 999 {
anyhow::bail!(
"input_fee_ppk must be between 0 and 999 (inclusive), got {}",
keyset_info.input_fee_ppk
);
}
let max_capacity = {
let after_stage1 = keyset_info.deterministic_value_after_fees(
funding_token_amount,
maximum_amount_for_one_output,
)?;
keyset_info
.deterministic_value_after_fees(after_stage1, maximum_amount_for_one_output)?
};
if capacity > max_capacity {
anyhow::bail!(
"capacity {} exceeds maximum achievable capacity {} for funding_token_amount {} \
(capacity must be <= forward(forward(funding_token_amount)))",
capacity,
max_capacity,
funding_token_amount
);
}
Ok(Self {
sender_pubkey,
receiver_pubkey,
mint,
unit,
capacity,
funding_token_amount,
expiry_timestamp,
setup_timestamp,
keyset_info,
maximum_amount_for_one_output,
channel_secret,
})
}
#[allow(clippy::too_many_arguments)]
pub fn new_with_secret_key(
sender_pubkey: cashu::nuts::PublicKey,
receiver_pubkey: cashu::nuts::PublicKey,
mint: String,
unit: CurrencyUnit,
capacity: u64,
funding_token_amount: u64,
expiry_timestamp: u64,
setup_timestamp: u64,
keyset_info: KeysetInfo,
maximum_amount_for_one_output: u64,
my_secret: &SecretKey,
) -> anyhow::Result<Self> {
let my_pubkey = my_secret.public_key();
let their_pubkey = if my_pubkey == sender_pubkey {
&receiver_pubkey
} else if my_pubkey == receiver_pubkey {
&sender_pubkey
} else {
anyhow::bail!(
"Secret key's public key doesn't match either sender_pubkey or receiver_pubkey"
);
};
let channel_secret = compute_channel_secret(my_secret, their_pubkey);
Self::new(
sender_pubkey,
receiver_pubkey,
mint,
unit,
capacity,
funding_token_amount,
expiry_timestamp,
setup_timestamp,
keyset_info,
maximum_amount_for_one_output,
channel_secret,
)
}
pub fn from_json_with_secret_key(
json_str: &str,
keyset_info: KeysetInfo,
my_secret: &SecretKey,
) -> anyhow::Result<Self> {
let json: serde_json::Value =
serde_json::from_str(json_str).map_err(|e| anyhow::anyhow!("Invalid JSON: {}", e))?;
let sender_pubkey_hex = json["sender_pubkey"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid 'sender_pubkey' field"))?;
let sender_pubkey: cashu::nuts::PublicKey = sender_pubkey_hex
.parse()
.map_err(|e| anyhow::anyhow!("Invalid sender_pubkey: {}", e))?;
let receiver_pubkey_hex = json["receiver_pubkey"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid 'receiver_pubkey' field"))?;
let receiver_pubkey: cashu::nuts::PublicKey = receiver_pubkey_hex
.parse()
.map_err(|e| anyhow::anyhow!("Invalid receiver_pubkey: {}", e))?;
let my_pubkey = my_secret.public_key();
let their_pubkey = if my_pubkey == sender_pubkey {
&receiver_pubkey
} else if my_pubkey == receiver_pubkey {
&sender_pubkey
} else {
anyhow::bail!(
"Secret key's public key doesn't match either sender_pubkey or receiver_pubkey"
);
};
let channel_secret = compute_channel_secret(my_secret, their_pubkey);
Self::from_json_with_channel_secret(json_str, keyset_info, channel_secret)
}
pub fn from_json_with_channel_secret(
json_str: &str,
keyset_info: KeysetInfo,
channel_secret: [u8; 32],
) -> anyhow::Result<Self> {
let json: serde_json::Value =
serde_json::from_str(json_str).map_err(|e| anyhow::anyhow!("Invalid JSON: {}", e))?;
let keyset_id_str = json["keyset_id"]
.as_str()
.or_else(|| json["keysetId"].as_str())
.ok_or_else(|| anyhow::anyhow!("Missing or invalid 'keyset_id' field"))?;
let json_keyset_id: cashu::nuts::Id = keyset_id_str
.parse()
.map_err(|e| anyhow::anyhow!("Invalid keyset_id: {}", e))?;
let json_input_fee_ppk = json["input_fee_ppk"]
.as_u64()
.or_else(|| json["inputFeePpk"].as_u64())
.ok_or_else(|| anyhow::anyhow!("Missing or invalid 'input_fee_ppk' field"))?;
if keyset_info.keyset_id != json_keyset_id {
anyhow::bail!(
"keyset_id mismatch: JSON has {}, KeysetInfo has {}",
json_keyset_id,
keyset_info.keyset_id
);
}
if keyset_info.input_fee_ppk != json_input_fee_ppk {
anyhow::bail!(
"input_fee_ppk mismatch: JSON has {}, KeysetInfo has {}",
json_input_fee_ppk,
keyset_info.input_fee_ppk
);
}
let mint = json["mint"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid 'mint' field"))?
.to_string();
let unit_str = json["unit"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid 'unit' field"))?;
let unit = match unit_str {
"sat" => CurrencyUnit::Sat,
"msat" => CurrencyUnit::Msat,
"usd" => CurrencyUnit::Usd,
"eur" => CurrencyUnit::Eur,
_ => anyhow::bail!("Unknown unit: {}", unit_str),
};
let capacity = json["capacity"]
.as_u64()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid 'capacity' field"))?;
let funding_token_amount = json["funding_token_amount"]
.as_u64()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid 'funding_token_amount' field"))?;
let maximum_amount_for_one_output = json["maximum_amount"]
.as_u64()
.or_else(|| json["maximum_amount_for_one_output"].as_u64())
.ok_or_else(|| anyhow::anyhow!("Missing or invalid 'maximum_amount' field"))?;
let setup_timestamp = json["setup_timestamp"]
.as_u64()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid 'setup_timestamp' field"))?;
let sender_pubkey_hex = json["sender_pubkey"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid 'sender_pubkey' field"))?;
let sender_pubkey: cashu::nuts::PublicKey = sender_pubkey_hex
.parse()
.map_err(|e| anyhow::anyhow!("Invalid sender_pubkey: {}", e))?;
let receiver_pubkey_hex = json["receiver_pubkey"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid 'receiver_pubkey' field"))?;
let receiver_pubkey: cashu::nuts::PublicKey = receiver_pubkey_hex
.parse()
.map_err(|e| anyhow::anyhow!("Invalid receiver_pubkey: {}", e))?;
let expiry_timestamp = json["expiry_timestamp"]
.as_u64()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid 'expiry_timestamp' field"))?;
Self::new(
sender_pubkey,
receiver_pubkey,
mint,
unit,
capacity,
funding_token_amount,
expiry_timestamp,
setup_timestamp,
keyset_info,
maximum_amount_for_one_output,
channel_secret,
)
}
pub fn get_capacity(&self) -> u64 {
self.capacity
}
pub fn get_channel_id_bytes(&self) -> [u8; 32] {
let params_string = format!(
"{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}",
self.mint,
self.unit_name(),
self.capacity,
self.funding_token_amount,
self.keyset_info.keyset_id,
self.keyset_info.input_fee_ppk,
self.maximum_amount_for_one_output,
self.setup_timestamp,
self.sender_pubkey.to_hex(),
self.receiver_pubkey.to_hex(),
self.expiry_timestamp,
hex::encode(self.channel_secret)
);
sha256::Hash::hash(params_string.as_bytes()).to_byte_array()
}
pub fn get_channel_id(&self) -> String {
hex::encode(self.get_channel_id_bytes())
}
pub fn get_channel_id_params_json(&self) -> String {
serde_json::json!({
"mint": self.mint,
"unit": self.unit_name(),
"capacity": self.capacity,
"funding_token_amount": self.funding_token_amount,
"keyset_id": self.keyset_info.keyset_id.to_string(),
"input_fee_ppk": self.keyset_info.input_fee_ppk,
"maximum_amount": self.maximum_amount_for_one_output,
"setup_timestamp": self.setup_timestamp,
"sender_pubkey": self.sender_pubkey.to_hex(),
"receiver_pubkey": self.receiver_pubkey.to_hex(),
"expiry_timestamp": self.expiry_timestamp
})
.to_string()
}
}
mod blinding;
#[cfg(test)]
mod tests;