use std::sync::LazyLock;
use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::hashes::{Hash, HashEngine};
use bitcoin::secp256k1::Secp256k1;
use thiserror::Error;
use crate::nuts::nut01::{PublicKey, SecretKey};
static SECP: LazyLock<Secp256k1<bitcoin::secp256k1::All>> = LazyLock::new(Secp256k1::new);
#[derive(Debug, Error)]
pub enum Error {
#[error("Invalid canonical slot {0}")]
InvalidCanonicalSlot(u8),
#[error("Invalid scalar hex string: {0}")]
InvalidScalarHex(String),
#[error("Scalar must be 32 bytes (64 hex chars), got {0}")]
InvalidScalarLength(usize),
#[error("Derived signing key is zero (invalid)")]
ZeroSigningKey,
#[error("Could not derive valid BIP340 signing key (neither k nor -k matched blinded pubkey)")]
NoValidBip340Key,
#[error(transparent)]
Secp256k1(#[from] bitcoin::secp256k1::Error),
#[error(transparent)]
Hex(#[from] crate::util::hex::Error),
#[error(transparent)]
NUT01(#[from] crate::nuts::nut01::Error),
}
#[allow(unused_variables)]
pub fn ecdh_kdf(
secret_key: &SecretKey,
pubkey: &PublicKey,
canonical_slot: u8,
) -> Result<SecretKey, Error> {
if canonical_slot > 10 {
return Err(Error::InvalidCanonicalSlot(canonical_slot));
}
let shared = pubkey.mul_tweak(&SECP, &secret_key.as_scalar())?;
let z_x: [u8; 32] = shared.x_only_public_key().0.serialize();
let mut engine = Sha256::engine();
engine.input(b"Cashu_P2BK_v1");
engine.input(&z_x);
engine.input(&[canonical_slot]);
let digest = Sha256::from_engine(engine.clone());
match SecretKey::from_slice(digest.as_byte_array()).map_err(Error::from) {
Ok(result) => Ok(result),
Err(_) => {
engine.input(&[0xFF]);
let digest = Sha256::from_engine(engine);
SecretKey::from_slice(digest.as_byte_array()).map_err(Error::from)
}
}
}
pub fn blind_public_key(pubkey: &PublicKey, r: &SecretKey) -> Result<PublicKey, Error> {
let r_pubkey = r.public_key();
Ok(pubkey.combine(&r_pubkey)?.into())
}
pub fn derive_signing_key_bip340(
privkey: &SecretKey,
r: &SecretKey,
blinded_pubkey: &PublicKey,
) -> Result<SecretKey, Error> {
let r_pubkey = r.public_key();
let r_pubkey_neg = r_pubkey.negate(&SECP);
let unblinded_pubkey = blinded_pubkey.combine(&r_pubkey_neg)?;
let privkey_pubkey = privkey.public_key();
let (unblinded_x_only, unblinded_parity) = unblinded_pubkey.x_only_public_key();
let privkey_x_only = privkey_pubkey.x_only_public_key();
let privkey_pubkey_parity = privkey_pubkey.to_bytes()[0] == 0x02;
let unblinded_parity_is_even = matches!(unblinded_parity, bitcoin::key::Parity::Even);
if unblinded_x_only != privkey_x_only {
return Err(Error::NoValidBip340Key);
}
match privkey_pubkey_parity == unblinded_parity_is_even {
true => Ok(privkey.add_tweak(&r.as_scalar())?.into()),
false => Ok(privkey.negate().add_tweak(&r.as_scalar())?.into()),
}
}
#[cfg(feature = "wallet")]
#[cfg(test)]
mod tests;
#[cfg(test)]
mod test_vectors;