use sha2::{Digest, Sha256};
use crate::error::AddressError;
use crate::network::Network;
use rustywallet_keys::public_key::PublicKey;
const SP_MAINNET_HRP: &str = "sp";
const SP_TESTNET_HRP: &str = "tsp";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SilentPaymentAddress {
scan_pubkey: [u8; 33],
spend_pubkey: [u8; 33],
network: Network,
encoded: String,
}
impl SilentPaymentAddress {
pub fn new(
scan_pubkey: &PublicKey,
spend_pubkey: &PublicKey,
network: Network,
) -> Result<Self, AddressError> {
if !network.is_bitcoin() {
return Err(AddressError::NetworkMismatch {
expected: "Bitcoin".to_string(),
actual: network.to_string(),
});
}
let scan_bytes = scan_pubkey.to_compressed();
let spend_bytes = spend_pubkey.to_compressed();
let mut scan_arr = [0u8; 33];
let mut spend_arr = [0u8; 33];
scan_arr.copy_from_slice(&scan_bytes);
spend_arr.copy_from_slice(&spend_bytes);
let hrp = Self::hrp(network);
let encoded = Self::encode_sp_address(hrp, &scan_arr, &spend_arr)?;
Ok(Self {
scan_pubkey: scan_arr,
spend_pubkey: spend_arr,
network,
encoded,
})
}
fn encode_sp_address(hrp: &str, scan: &[u8; 33], spend: &[u8; 33]) -> Result<String, AddressError> {
use bech32::{Bech32m, Hrp};
let hrp = Hrp::parse(hrp).map_err(|e| AddressError::InvalidBech32(e.to_string()))?;
let mut payload = Vec::with_capacity(66);
payload.extend_from_slice(scan);
payload.extend_from_slice(spend);
let mut data_5bit = Vec::with_capacity(1 + payload.len() * 8 / 5 + 1);
data_5bit.push(0u8);
let converted = Self::convert_bits(&payload, 8, 5, true)?;
data_5bit.extend(converted);
bech32::encode::<Bech32m>(hrp, &data_5bit)
.map_err(|e| AddressError::InvalidBech32(e.to_string()))
}
fn convert_bits(data: &[u8], from_bits: u32, to_bits: u32, pad: bool) -> Result<Vec<u8>, AddressError> {
let mut acc: u32 = 0;
let mut bits: u32 = 0;
let mut ret = Vec::new();
let maxv: u32 = (1 << to_bits) - 1;
for &value in data {
let value = value as u32;
if (value >> from_bits) != 0 {
return Err(AddressError::InvalidFormat("Invalid bit conversion".into()));
}
acc = (acc << from_bits) | value;
bits += from_bits;
while bits >= to_bits {
bits -= to_bits;
ret.push(((acc >> bits) & maxv) as u8);
}
}
if pad {
if bits > 0 {
ret.push(((acc << (to_bits - bits)) & maxv) as u8);
}
} else if bits >= from_bits || ((acc << (to_bits - bits)) & maxv) != 0 {
return Err(AddressError::InvalidFormat("Invalid padding".into()));
}
Ok(ret)
}
pub fn from_single_key(pubkey: &PublicKey, network: Network) -> Result<Self, AddressError> {
Self::new(pubkey, pubkey, network)
}
#[inline]
pub fn scan_pubkey(&self) -> &[u8; 33] {
&self.scan_pubkey
}
#[inline]
pub fn spend_pubkey(&self) -> &[u8; 33] {
&self.spend_pubkey
}
#[inline]
pub fn network(&self) -> Network {
self.network
}
pub fn parse(s: &str) -> Result<Self, AddressError> {
s.parse()
}
pub fn validate(s: &str) -> Result<(), AddressError> {
s.parse::<Self>().map(|_| ())
}
fn hrp(network: Network) -> &'static str {
match network {
Network::BitcoinMainnet => SP_MAINNET_HRP,
Network::BitcoinTestnet => SP_TESTNET_HRP,
_ => SP_MAINNET_HRP,
}
}
pub fn is_single_key(&self) -> bool {
self.scan_pubkey == self.spend_pubkey
}
}
impl std::fmt::Display for SilentPaymentAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.encoded)
}
}
impl std::str::FromStr for SilentPaymentAddress {
type Err = AddressError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (hrp, data) = bech32::decode(s)
.map_err(|e| AddressError::InvalidBech32(e.to_string()))?;
let network = match hrp.as_str() {
SP_MAINNET_HRP => Network::BitcoinMainnet,
SP_TESTNET_HRP => Network::BitcoinTestnet,
_ => {
return Err(AddressError::InvalidFormat(format!(
"Unknown SP HRP: {}",
hrp
)))
}
};
if data.is_empty() {
return Err(AddressError::InvalidFormat("Empty SP data".into()));
}
let version = data[0];
if version != 0 {
return Err(AddressError::InvalidFormat(format!(
"Invalid SP version: expected 0, got {}",
version
)));
}
let bytes = Self::convert_bits(&data[1..], 5, 8, false)?;
if bytes.len() < 66 {
return Err(AddressError::InvalidFormat(format!(
"Invalid SP data length: expected 66, got {}",
bytes.len()
)));
}
let mut scan_pubkey = [0u8; 33];
let mut spend_pubkey = [0u8; 33];
scan_pubkey.copy_from_slice(&bytes[0..33]);
spend_pubkey.copy_from_slice(&bytes[33..66]);
Ok(Self {
scan_pubkey,
spend_pubkey,
network,
encoded: s.to_lowercase(),
})
}
}
pub struct SilentPaymentDeriver;
impl SilentPaymentDeriver {
pub fn compute_tweak(
scan_pubkey: &[u8; 33],
input_hash: &[u8; 32],
output_index: u32,
) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(scan_pubkey);
hasher.update(input_hash);
hasher.update(output_index.to_le_bytes());
let result = hasher.finalize();
let mut tweak = [0u8; 32];
tweak.copy_from_slice(&result);
tweak
}
pub fn hash_outpoints(outpoints: &[(String, u32)]) -> [u8; 32] {
let mut hasher = Sha256::new();
let mut sorted: Vec<_> = outpoints.to_vec();
sorted.sort();
for (txid, vout) in sorted {
if let Ok(txid_bytes) = hex::decode(&txid) {
hasher.update(&txid_bytes);
hasher.update(vout.to_le_bytes());
}
}
let result = hasher.finalize();
let mut hash = [0u8; 32];
hash.copy_from_slice(&result);
hash
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SilentPaymentLabel {
value: [u8; 32],
}
impl SilentPaymentLabel {
pub fn new(m: u32) -> Self {
let mut hasher = Sha256::new();
hasher.update(b"BIP0352/Label");
hasher.update(m.to_le_bytes());
let result = hasher.finalize();
let mut value = [0u8; 32];
value.copy_from_slice(&result);
Self { value }
}
pub fn value(&self) -> &[u8; 32] {
&self.value
}
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self { value: bytes }
}
}
#[cfg(test)]
mod tests {
use super::*;
use rustywallet_keys::private_key::PrivateKey;
#[test]
fn test_silent_payment_address_creation() {
let scan_key = PrivateKey::random();
let spend_key = PrivateKey::random();
let addr = SilentPaymentAddress::new(
&scan_key.public_key(),
&spend_key.public_key(),
Network::BitcoinMainnet,
).unwrap();
assert!(addr.to_string().starts_with("sp1"));
assert!(!addr.is_single_key());
}
#[test]
fn test_silent_payment_single_key() {
let key = PrivateKey::random();
let addr = SilentPaymentAddress::from_single_key(
&key.public_key(),
Network::BitcoinMainnet,
).unwrap();
assert!(addr.is_single_key());
assert_eq!(addr.scan_pubkey(), addr.spend_pubkey());
}
#[test]
fn test_silent_payment_testnet() {
let key = PrivateKey::random();
let addr = SilentPaymentAddress::from_single_key(
&key.public_key(),
Network::BitcoinTestnet,
).unwrap();
assert!(addr.to_string().starts_with("tsp1"));
}
#[test]
fn test_silent_payment_label() {
let label0 = SilentPaymentLabel::new(0);
let label1 = SilentPaymentLabel::new(1);
assert_ne!(label0.value(), label1.value());
assert_eq!(label0.value().len(), 32);
}
#[test]
fn test_hash_outpoints() {
let outpoints = vec![
("abc123".to_string(), 0),
("def456".to_string(), 1),
];
let hash = SilentPaymentDeriver::hash_outpoints(&outpoints);
assert_eq!(hash.len(), 32);
}
#[test]
fn test_compute_tweak() {
let scan_pubkey = [0u8; 33];
let input_hash = [1u8; 32];
let tweak = SilentPaymentDeriver::compute_tweak(&scan_pubkey, &input_hash, 0);
assert_eq!(tweak.len(), 32);
let tweak2 = SilentPaymentDeriver::compute_tweak(&scan_pubkey, &input_hash, 1);
assert_ne!(tweak, tweak2);
}
}