use crate::error::{Result, StealthError};
use crate::keys::PublicMetaAddress;
use curve25519_dalek::edwards::CompressedEdwardsY;
use curve25519_dalek::scalar::Scalar;
use curve25519_dalek::constants::ED25519_BASEPOINT_POINT;
use rand::RngCore;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::keypair_from_seed;
use solana_sdk::signer::Signer;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StealthPayment {
pub stealth_address: Pubkey,
pub ephemeral_pubkey: [u8; 32],
pub amount: u64,
}
pub struct EphemeralKeypair {
private_key: Scalar,
pub public_key: [u8; 32],
}
impl EphemeralKeypair {
pub fn generate() -> Self {
let mut rng = rand::thread_rng();
let mut seed = [0u8; 32];
rng.fill_bytes(&mut seed);
let private_key = Scalar::from_bytes_mod_order(seed);
let public_point = &private_key * &ED25519_BASEPOINT_POINT;
let public_key = public_point.compress().to_bytes();
Self {
private_key,
public_key,
}
}
pub fn compute_shared_secret(&self, viewing_pubkey: &[u8; 32]) -> Result<[u8; 32]> {
let viewing_point = CompressedEdwardsY(*viewing_pubkey)
.decompress()
.ok_or_else(|| StealthError::InvalidPublicKey("Invalid viewing pubkey".to_string()))?;
let shared_point = viewing_point * self.private_key;
Ok(shared_point.compress().to_bytes())
}
}
impl StealthPayment {
pub fn create(recipient: &PublicMetaAddress, amount: u64) -> Result<Self> {
let ephemeral = EphemeralKeypair::generate();
let shared_secret = ephemeral.compute_shared_secret(recipient.viewing_pubkey())?;
let stealth_address = derive_stealth_address(recipient.spending_pubkey(), &shared_secret)?;
Ok(Self {
stealth_address,
ephemeral_pubkey: ephemeral.public_key,
amount,
})
}
}
pub fn derive_stealth_seed(
spending_pubkey: &[u8; 32],
shared_secret: &[u8; 32],
) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(b"solana-stealth-seed-v1");
hasher.update(spending_pubkey);
hasher.update(shared_secret);
hasher.finalize().into()
}
pub fn derive_stealth_address(
spending_pubkey: &[u8; 32],
shared_secret: &[u8; 32],
) -> Result<Pubkey> {
let seed = derive_stealth_seed(spending_pubkey, shared_secret);
let keypair = keypair_from_seed(&seed)
.map_err(|e| StealthError::CryptoError(format!("Failed to derive keypair: {}", e)))?;
Ok(keypair.pubkey())
}
pub fn check_stealth_address(
viewing_key: &[u8; 32],
spending_pubkey: &[u8; 32],
ephemeral_pubkey: &[u8; 32],
stealth_address: &Pubkey,
) -> Result<bool> {
let ephemeral_point = CompressedEdwardsY(*ephemeral_pubkey)
.decompress()
.ok_or_else(|| StealthError::InvalidEphemeralKey)?;
let viewing_scalar = Scalar::from_bytes_mod_order(*viewing_key);
let shared_point = ephemeral_point * viewing_scalar;
let shared_secret = shared_point.compress().to_bytes();
let expected_address = derive_stealth_address(spending_pubkey, &shared_secret)?;
Ok(expected_address == *stealth_address)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keys::StealthMetaAddress;
#[test]
fn test_create_stealth_payment() {
let meta = StealthMetaAddress::generate();
let public_meta = meta.public_meta_address();
let payment = StealthPayment::create(&public_meta, 1_000_000_000).unwrap();
assert_ne!(payment.stealth_address, Pubkey::default());
assert_ne!(payment.ephemeral_pubkey, [0u8; 32]);
assert_eq!(payment.amount, 1_000_000_000);
}
#[test]
fn test_stealth_address_detection() {
let meta = StealthMetaAddress::generate();
let public_meta = meta.public_meta_address();
let payment = StealthPayment::create(&public_meta, 1_000_000_000).unwrap();
let is_mine = check_stealth_address(
meta.viewing_key(),
meta.spending_pubkey(),
&payment.ephemeral_pubkey,
&payment.stealth_address,
)
.unwrap();
assert!(is_mine, "Receiver should detect their own payment");
}
#[test]
fn test_different_receiver_cannot_detect() {
let alice = StealthMetaAddress::generate();
let bob = StealthMetaAddress::generate();
let payment = StealthPayment::create(&alice.public_meta_address(), 1_000_000_000).unwrap();
let is_bobs = check_stealth_address(
bob.viewing_key(),
bob.spending_pubkey(),
&payment.ephemeral_pubkey,
&payment.stealth_address,
)
.unwrap();
assert!(!is_bobs, "Bob should not detect Alice's payment");
}
#[test]
fn test_each_payment_unique_address() {
let meta = StealthMetaAddress::generate();
let public_meta = meta.public_meta_address();
let payment1 = StealthPayment::create(&public_meta, 1_000_000_000).unwrap();
let payment2 = StealthPayment::create(&public_meta, 1_000_000_000).unwrap();
assert_ne!(
payment1.stealth_address, payment2.stealth_address,
"Each payment should have a unique stealth address"
);
assert_ne!(payment1.ephemeral_pubkey, payment2.ephemeral_pubkey);
}
}