use crate::error::BitcoinError;
use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey};
use bitcoin::{Address, Network, TxOut};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SilentPaymentAddress {
pub scan_pubkey: PublicKey,
pub spend_pubkey: PublicKey,
pub label: Option<u32>,
pub network: Network,
}
impl SilentPaymentAddress {
pub fn new(
scan_pubkey: PublicKey,
spend_pubkey: PublicKey,
label: Option<u32>,
network: Network,
) -> Self {
Self {
scan_pubkey,
spend_pubkey,
label,
network,
}
}
pub fn encode(&self) -> String {
let prefix = match self.network {
Network::Bitcoin => "sp1q",
Network::Testnet => "tsp1q",
Network::Signet => "ssp1q",
Network::Regtest => "rsp1q",
_ => "sp1q",
};
format!(
"{}{}{}",
prefix,
hex::encode(self.scan_pubkey.serialize()),
hex::encode(self.spend_pubkey.serialize())
)
}
pub fn from_string(s: &str) -> Result<Self, BitcoinError> {
let (network, prefix_len) = if s.starts_with("sp1q") {
(Network::Bitcoin, 4)
} else if s.starts_with("tsp1q") {
(Network::Testnet, 5)
} else if s.starts_with("ssp1q") {
(Network::Signet, 5)
} else if s.starts_with("rsp1q") {
(Network::Regtest, 5)
} else {
return Err(BitcoinError::InvalidAddress(
"Invalid silent payment address prefix".to_string(),
));
};
let hex_data = &s[prefix_len..];
if hex_data.len() != 132 {
return Err(BitcoinError::InvalidAddress(format!(
"Invalid address length: expected 132 hex chars, got {}",
hex_data.len()
)));
}
let scan_bytes = hex::decode(&hex_data[..66])
.map_err(|e| BitcoinError::InvalidAddress(format!("Invalid scan pubkey hex: {}", e)))?;
let scan_pubkey = PublicKey::from_slice(&scan_bytes)
.map_err(|e| BitcoinError::InvalidAddress(format!("Invalid scan public key: {}", e)))?;
let spend_bytes = hex::decode(&hex_data[66..]).map_err(|e| {
BitcoinError::InvalidAddress(format!("Invalid spend pubkey hex: {}", e))
})?;
let spend_pubkey = PublicKey::from_slice(&spend_bytes).map_err(|e| {
BitcoinError::InvalidAddress(format!("Invalid spend public key: {}", e))
})?;
Ok(Self {
scan_pubkey,
spend_pubkey,
label: None, network,
})
}
}
impl std::fmt::Display for SilentPaymentAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.encode())
}
}
#[derive(Debug)]
pub struct SilentPaymentWallet {
scan_privkey: SecretKey,
spend_privkey: SecretKey,
network: Network,
secp: Secp256k1<bitcoin::secp256k1::All>,
}
impl SilentPaymentWallet {
pub fn new() -> Result<Self, BitcoinError> {
Self::new_with_network(Network::Bitcoin)
}
pub fn new_with_network(network: Network) -> Result<Self, BitcoinError> {
use bitcoin::secp256k1::rand::rngs::OsRng;
let secp = Secp256k1::new();
let scan_privkey = SecretKey::new(&mut OsRng);
let spend_privkey = SecretKey::new(&mut OsRng);
Ok(Self {
scan_privkey,
spend_privkey,
network,
secp,
})
}
pub fn from_keys(scan_privkey: SecretKey, spend_privkey: SecretKey, network: Network) -> Self {
Self {
scan_privkey,
spend_privkey,
network,
secp: Secp256k1::new(),
}
}
pub fn get_address(&self, label: Option<u32>) -> Result<SilentPaymentAddress, BitcoinError> {
let scan_pubkey = PublicKey::from_secret_key(&self.secp, &self.scan_privkey);
let mut spend_pubkey = PublicKey::from_secret_key(&self.secp, &self.spend_privkey);
if let Some(label_value) = label {
let label_tweak = self.compute_label_tweak(label_value)?;
spend_pubkey = spend_pubkey
.add_exp_tweak(&self.secp, &label_tweak)
.map_err(|e| {
BitcoinError::InvalidAddress(format!("Failed to apply label tweak: {}", e))
})?;
}
Ok(SilentPaymentAddress::new(
scan_pubkey,
spend_pubkey,
label,
self.network,
))
}
fn compute_label_tweak(&self, label: u32) -> Result<Scalar, BitcoinError> {
use bitcoin::hashes::{Hash, HashEngine, sha256};
let mut engine = sha256::Hash::engine();
engine.input(&self.scan_privkey.secret_bytes());
engine.input(&label.to_le_bytes());
let hash = sha256::Hash::from_engine(engine);
Scalar::from_be_bytes(hash.to_byte_array())
.map_err(|_| BitcoinError::InvalidAddress("Failed to compute label tweak".to_string()))
}
pub fn scan_pubkey(&self) -> PublicKey {
PublicKey::from_secret_key(&self.secp, &self.scan_privkey)
}
pub fn spend_pubkey(&self) -> PublicKey {
PublicKey::from_secret_key(&self.secp, &self.spend_privkey)
}
}
#[derive(Debug)]
pub struct SilentPaymentSender {
secp: Secp256k1<bitcoin::secp256k1::All>,
}
impl SilentPaymentSender {
pub fn new() -> Self {
Self {
secp: Secp256k1::new(),
}
}
pub fn create_output(
&self,
recipient: &SilentPaymentAddress,
input_privkeys: &[SecretKey],
output_index: u32,
amount: u64,
) -> Result<TxOut, BitcoinError> {
let shared_secret =
self.compute_shared_secret(&recipient.scan_pubkey, input_privkeys, output_index)?;
let output_pubkey = recipient
.spend_pubkey
.add_exp_tweak(&self.secp, &shared_secret)
.map_err(|e| {
BitcoinError::InvalidAddress(format!("Failed to derive output key: {}", e))
})?;
let address = Address::p2tr(
&self.secp,
output_pubkey.x_only_public_key().0,
None,
recipient.network,
);
Ok(TxOut {
value: bitcoin::Amount::from_sat(amount),
script_pubkey: address.script_pubkey(),
})
}
fn compute_shared_secret(
&self,
scan_pubkey: &PublicKey,
input_privkeys: &[SecretKey],
output_index: u32,
) -> Result<Scalar, BitcoinError> {
use bitcoin::hashes::{Hash, HashEngine, sha256};
let mut sum = input_privkeys[0];
for privkey in &input_privkeys[1..] {
sum = sum.add_tweak(&(*privkey).into()).map_err(|e| {
BitcoinError::InvalidAddress(format!("Failed to sum private keys: {}", e))
})?;
}
let ecdh_point = scan_pubkey
.mul_tweak(&self.secp, &sum.into())
.map_err(|e| BitcoinError::InvalidAddress(format!("ECDH computation failed: {}", e)))?;
let mut engine = sha256::Hash::engine();
engine.input(&ecdh_point.serialize());
engine.input(&output_index.to_le_bytes());
let hash = sha256::Hash::from_engine(engine);
Scalar::from_be_bytes(hash.to_byte_array()).map_err(|_| {
BitcoinError::InvalidAddress("Failed to compute shared secret".to_string())
})
}
}
impl Default for SilentPaymentSender {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct SilentPaymentScanner {
#[allow(dead_code)]
wallet: SilentPaymentWallet,
detected_outputs: HashMap<bitcoin::Txid, Vec<DetectedOutput>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetectedOutput {
pub txid: bitcoin::Txid,
pub vout: u32,
pub amount: u64,
pub spend_privkey: SecretKey,
pub label: Option<u32>,
}
impl SilentPaymentScanner {
pub fn new(wallet: SilentPaymentWallet) -> Self {
Self {
wallet,
detected_outputs: HashMap::new(),
}
}
pub fn scan_transaction(
&mut self,
tx: &bitcoin::Transaction,
) -> Result<Vec<DetectedOutput>, BitcoinError> {
let mut detected = Vec::new();
let input_pubkeys = self.extract_input_pubkeys(tx)?;
for (vout, output) in tx.output.iter().enumerate() {
if let Some(detected_output) =
self.check_output(tx.compute_txid(), vout as u32, output, &input_pubkeys)?
{
detected.push(detected_output);
}
}
if !detected.is_empty() {
self.detected_outputs
.insert(tx.compute_txid(), detected.clone());
}
Ok(detected)
}
fn extract_input_pubkeys(
&self,
tx: &bitcoin::Transaction,
) -> Result<Vec<PublicKey>, BitcoinError> {
let mut pubkeys = Vec::new();
for input in &tx.input {
if !input.witness.is_empty() {
if input.witness.len() == 2 {
let pubkey_bytes = input.witness.nth(1).unwrap();
if let Ok(pk) = PublicKey::from_slice(pubkey_bytes) {
pubkeys.push(pk);
continue;
}
}
}
let script_sig = &input.script_sig;
if !script_sig.is_empty() {
for instruction in script_sig.instructions() {
if let Ok(bitcoin::blockdata::script::Instruction::PushBytes(bytes)) =
instruction
{
if let Ok(pk) = PublicKey::from_slice(bytes.as_bytes()) {
pubkeys.push(pk);
}
}
}
}
}
Ok(pubkeys)
}
fn check_output(
&self,
txid: bitcoin::Txid,
vout: u32,
output: &TxOut,
input_pubkeys: &[PublicKey],
) -> Result<Option<DetectedOutput>, BitcoinError> {
use bitcoin::hashes::{Hash, HashEngine, sha256};
if input_pubkeys.is_empty() {
return Ok(None);
}
let output_pk = if output.script_pubkey.is_p2tr() {
let script_bytes = output.script_pubkey.as_bytes();
if script_bytes.len() != 34 {
return Ok(None);
}
let mut xonly_bytes = [0u8; 32];
xonly_bytes.copy_from_slice(&script_bytes[2..34]);
match bitcoin::secp256k1::XOnlyPublicKey::from_slice(&xonly_bytes) {
Ok(pk) => pk,
Err(_) => return Ok(None), }
} else {
return Ok(None);
};
let secp = bitcoin::secp256k1::Secp256k1::new();
let mut sum_pk = input_pubkeys[0];
for pk in &input_pubkeys[1..] {
sum_pk = sum_pk.combine(pk).map_err(|e| {
BitcoinError::InvalidAddress(format!("Failed to sum input pubkeys: {}", e))
})?;
}
let ecdh_point = sum_pk
.mul_tweak(&secp, &self.wallet.scan_privkey.into())
.map_err(|e| BitcoinError::InvalidAddress(format!("ECDH failed: {}", e)))?;
let labels_to_try = vec![None, Some(0), Some(1), Some(2), Some(3)];
for label in labels_to_try {
let mut engine = sha256::Hash::engine();
engine.input(&ecdh_point.serialize());
engine.input(&vout.to_le_bytes());
let hash = sha256::Hash::from_engine(engine);
let shared_secret = bitcoin::secp256k1::Scalar::from_be_bytes(hash.to_byte_array())
.map_err(|_| {
BitcoinError::InvalidAddress("Failed to compute shared secret".to_string())
})?;
let mut spend_pk =
bitcoin::secp256k1::PublicKey::from_secret_key(&secp, &self.wallet.spend_privkey);
if let Some(label_value) = label {
let label_tweak = self.wallet.compute_label_tweak(label_value)?;
spend_pk = spend_pk.add_exp_tweak(&secp, &label_tweak).map_err(|e| {
BitcoinError::InvalidAddress(format!("Failed to apply label tweak: {}", e))
})?;
}
let expected_pk = spend_pk.add_exp_tweak(&secp, &shared_secret).map_err(|e| {
BitcoinError::InvalidAddress(format!("Failed to derive output key: {}", e))
})?;
if expected_pk.x_only_public_key().0 == output_pk {
let spend_privkey = self
.wallet
.spend_privkey
.add_tweak(&shared_secret)
.map_err(|e| {
BitcoinError::InvalidAddress(format!(
"Failed to derive spend privkey: {}",
e
))
})?;
return Ok(Some(DetectedOutput {
txid,
vout,
amount: output.value.to_sat(),
spend_privkey,
label,
}));
}
}
Ok(None)
}
pub fn get_detected_outputs(&self) -> Vec<&DetectedOutput> {
self.detected_outputs
.values()
.flat_map(|outputs| outputs.iter())
.collect()
}
pub fn get_balance(&self) -> u64 {
self.get_detected_outputs()
.iter()
.map(|output| output.amount)
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_silent_payment_wallet_creation() {
let wallet = SilentPaymentWallet::new().unwrap();
let address = wallet.get_address(None).unwrap();
assert_eq!(address.network, Network::Bitcoin);
assert_eq!(address.label, None);
assert!(address.encode().starts_with("sp1q"));
}
#[test]
fn test_silent_payment_with_label() {
let wallet = SilentPaymentWallet::new().unwrap();
let address_no_label = wallet.get_address(None).unwrap();
let address_with_label = wallet.get_address(Some(1)).unwrap();
assert_ne!(
address_no_label.spend_pubkey,
address_with_label.spend_pubkey
);
assert_eq!(address_no_label.scan_pubkey, address_with_label.scan_pubkey);
}
#[test]
fn test_silent_payment_networks() {
let networks = vec![
Network::Bitcoin,
Network::Testnet,
Network::Signet,
Network::Regtest,
];
for network in networks {
let wallet = SilentPaymentWallet::new_with_network(network).unwrap();
let address = wallet.get_address(None).unwrap();
assert_eq!(address.network, network);
}
}
#[test]
fn test_sender_create_output() {
use bitcoin::secp256k1::rand::rngs::OsRng;
let wallet = SilentPaymentWallet::new().unwrap();
let address = wallet.get_address(None).unwrap();
let sender = SilentPaymentSender::new();
let input_key = SecretKey::new(&mut OsRng);
let output = sender
.create_output(&address, &[input_key], 0, 100_000)
.unwrap();
assert_eq!(output.value.to_sat(), 100_000);
assert!(!output.script_pubkey.is_empty());
}
#[test]
fn test_scanner_creation() {
let wallet = SilentPaymentWallet::new().unwrap();
let scanner = SilentPaymentScanner::new(wallet);
assert_eq!(scanner.get_balance(), 0);
assert!(scanner.get_detected_outputs().is_empty());
}
#[test]
fn test_address_to_string() {
let wallet = SilentPaymentWallet::new_with_network(Network::Testnet).unwrap();
let address = wallet.get_address(None).unwrap();
let addr_string = address.encode();
assert!(addr_string.starts_with("tsp1q"));
assert!(addr_string.len() > 10);
}
#[test]
fn test_address_round_trip() {
let wallet = SilentPaymentWallet::new_with_network(Network::Bitcoin).unwrap();
let address = wallet.get_address(None).unwrap();
let addr_string = address.encode();
let parsed_address = SilentPaymentAddress::from_string(&addr_string).unwrap();
assert_eq!(parsed_address.scan_pubkey, address.scan_pubkey);
assert_eq!(parsed_address.spend_pubkey, address.spend_pubkey);
assert_eq!(parsed_address.network, address.network);
}
#[test]
fn test_invalid_address_prefix() {
let result = SilentPaymentAddress::from_string("invalid123");
assert!(result.is_err());
}
#[test]
fn test_invalid_address_length() {
let result = SilentPaymentAddress::from_string("sp1q12345");
assert!(result.is_err());
}
}