//! Confidential Transactions //! //! This module combines all privacy primitives into a complete confidential //! transaction system: //! //! - **Ring Signatures**: Hide which input is being spent //! - **Stealth Addresses**: Hide the recipient //! - **Pedersen Commitments**: Hide amounts //! - **Bulletproofs**: Prove amounts are valid //! //! ## Transaction Structure //! //! ```text //! ┌──────────────────────────────────────────────────────────────┐ //! │ Confidential Transaction │ //! ├──────────────────────────────────────────────────────────────┤ //! │ Inputs: │ //! │ ├─ Ring Signature (proves ownership, hides which key) │ //! │ ├─ Key Image (prevents double-spend) │ //! │ └─ Amount Commitment (hidden amount) │ //! ├──────────────────────────────────────────────────────────────┤ //! │ Outputs: │ //! │ ├─ Stealth Address (one-time recipient address) │ //! │ ├─ Ephemeral Public Key (for recipient to derive key) │ //! │ ├─ Amount Commitment (hidden amount) │ //! │ └─ Range Proof (proves 0 ≤ amount < 2^64) │ //! ├──────────────────────────────────────────────────────────────┤ //! │ Balance Proof: │ //! │ └─ sum(input_commitments) = sum(output_commitments) + fee │ //! └──────────────────────────────────────────────────────────────┘ //! ``` //! //! ## Verification //! //! 1. Check all ring signatures are valid //! 2. Check all key images are unused (no double-spend) //! 3. Check all range proofs are valid //! 4. Check the balance equation holds use alloc::vec::Vec; use curve25519_dalek::scalar::Scalar; use rand_core::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; use borsh::{BorshSerialize, BorshDeserialize}; use sha2::{Sha512, Digest}; use crate::{ bulletproofs::RangeProof, pedersen::{BlindingFactor, CommitmentBatch, CommitmentBytes, PedersenCommitment}, ring::{KeyImage, RingPrivateKey, RingPublicKey, RingSignature}, stealth::{StealthAddress, StealthMetaAddress}, Error, Result, DOMAIN_SEPARATOR, }; /// Generate a random scalar using the provided RNG fn random_scalar(rng: &mut R) -> Scalar { let mut bytes = [0u8; 64]; rng.fill_bytes(&mut bytes); Scalar::from_bytes_mod_order_wide(&bytes) } /// A confidential transaction input #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct ConfidentialInput { /// Ring signature proving ownership pub ring_signature: RingSignature, /// Ring of public keys (decoys + real) pub ring: Vec, /// Amount commitment (from the output being spent) pub amount_commitment: CommitmentBytes, /// Reference to the output being spent (transaction hash + output index) pub output_reference: OutputReference, } impl ConfidentialInput { /// Create a new confidential input pub fn create( // The key to spend spending_key: &RingPrivateKey, // The ring of public keys (must include spending key's pubkey) ring: Vec, // Index of the real key in the ring real_index: usize, // The amount commitment from the output amount_commitment: PedersenCommitment, // Reference to the output output_reference: OutputReference, // Transaction message to sign message: &[u8], rng: &mut R, ) -> Result { let ring_signature = RingSignature::sign( spending_key, &ring, real_index, message, rng, )?; Ok(Self { ring_signature, ring, amount_commitment: CommitmentBytes::from(&amount_commitment), output_reference, }) } /// Get the key image pub fn key_image(&self) -> &KeyImage { &self.ring_signature.key_image } /// Verify the input pub fn verify(&self, message: &[u8]) -> Result { self.ring_signature.verify(&self.ring, message) } /// Get the amount commitment pub fn commitment(&self) -> Result { self.amount_commitment.clone().try_into() } } /// A confidential transaction output #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct ConfidentialOutput { /// Stealth address (one-time address for recipient) pub stealth_address: StealthAddress, /// Amount commitment (hidden amount) pub amount_commitment: CommitmentBytes, /// Range proof (proves amount is valid) pub range_proof: RangeProof, /// Optional encrypted amount (for recipient to decrypt) pub encrypted_amount: Option, } impl ConfidentialOutput { /// Create a new confidential output pub fn create( recipient: &StealthMetaAddress, amount: u64, rng: &mut R, ) -> Result<(Self, BlindingFactor)> { // Generate stealth address let stealth_address = StealthAddress::generate(recipient, rng); // Create commitment and range proof let blinding = BlindingFactor::random(rng); let (range_proof, commitment) = RangeProof::prove(amount, &blinding, rng)?; // Encrypt amount for recipient let encrypted_amount = Some(EncryptedAmount::encrypt( amount, &stealth_address, &blinding, )); Ok(( Self { stealth_address, amount_commitment: CommitmentBytes::from(&commitment), range_proof, encrypted_amount, }, blinding, )) } /// Verify the output (range proof) pub fn verify(&self) -> Result { let verified_commitment = self.range_proof.verify()?; // Commitment from proof should match stored commitment let stored_commitment: PedersenCommitment = self.amount_commitment.clone().try_into()?; if verified_commitment != stored_commitment { return Err(Error::InvalidTransaction( "Commitment mismatch in output".into(), )); } Ok(verified_commitment) } /// Get the amount commitment pub fn commitment(&self) -> Result { self.amount_commitment.clone().try_into() } } /// Reference to a previous output #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct OutputReference { /// Transaction hash pub tx_hash: [u8; 32], /// Output index within the transaction pub output_index: u32, } impl OutputReference { /// Create a new output reference pub fn new(tx_hash: [u8; 32], output_index: u32) -> Self { Self { tx_hash, output_index } } } /// Encrypted amount (for recipient to decrypt) #[derive(Clone, Debug)] pub struct EncryptedAmount { /// Encrypted data (amount || blinding factor, encrypted with shared secret) pub ciphertext: [u8; 40], // 8 bytes amount + 32 bytes blinding } impl serde::Serialize for EncryptedAmount { fn serialize(&self, serializer: S) -> core::result::Result where S: serde::Serializer, { serializer.serialize_bytes(&self.ciphertext) } } impl<'de> serde::Deserialize<'de> for EncryptedAmount { fn deserialize(deserializer: D) -> core::result::Result where D: serde::Deserializer<'de>, { let bytes: Vec = serde::Deserialize::deserialize(deserializer)?; if bytes.len() != 40 { return Err(serde::de::Error::custom("EncryptedAmount must be 40 bytes")); } let mut ciphertext = [0u8; 40]; ciphertext.copy_from_slice(&bytes); Ok(Self { ciphertext }) } } impl BorshSerialize for EncryptedAmount { fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { writer.write_all(&self.ciphertext) } } impl BorshDeserialize for EncryptedAmount { fn deserialize_reader(reader: &mut R) -> borsh::io::Result { let mut ciphertext = [0u8; 40]; reader.read_exact(&mut ciphertext)?; Ok(Self { ciphertext }) } } impl EncryptedAmount { /// Encrypt an amount for a stealth address recipient pub fn encrypt( amount: u64, stealth_address: &StealthAddress, blinding: &BlindingFactor, ) -> Self { // Derive encryption key from stealth address components let mut hasher = Sha512::new(); hasher.update(DOMAIN_SEPARATOR); hasher.update(b"AMOUNT_ENCRYPT"); hasher.update(&stealth_address.to_bytes()); let key_material = hasher.finalize(); // Simple XOR encryption (in production, use ChaCha20-Poly1305) let mut plaintext = [0u8; 40]; plaintext[..8].copy_from_slice(&amount.to_le_bytes()); plaintext[8..].copy_from_slice(&blinding.to_bytes()); let mut ciphertext = [0u8; 40]; for i in 0..40 { ciphertext[i] = plaintext[i] ^ key_material[i]; } Self { ciphertext } } /// Decrypt the amount using a stealth spending key pub fn decrypt( &self, stealth_address: &StealthAddress, ) -> Option<(u64, BlindingFactor)> { // Derive decryption key let mut hasher = Sha512::new(); hasher.update(DOMAIN_SEPARATOR); hasher.update(b"AMOUNT_ENCRYPT"); hasher.update(&stealth_address.to_bytes()); let key_material = hasher.finalize(); // XOR decrypt let mut plaintext = [0u8; 40]; for i in 0..40 { plaintext[i] = self.ciphertext[i] ^ key_material[i]; } let amount = u64::from_le_bytes(plaintext[..8].try_into().ok()?); let blinding_bytes: [u8; 32] = plaintext[8..].try_into().ok()?; let blinding = BlindingFactor::from_bytes(&blinding_bytes).ok()?; Some((amount, blinding)) } } /// A complete confidential transaction #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct ConfidentialTransaction { /// Transaction version pub version: u8, /// Inputs (what's being spent) pub inputs: Vec, /// Outputs (where value goes) pub outputs: Vec, /// Transaction fee (plaintext, goes to block producer) pub fee: u64, /// Excess signature (proves balance without revealing amounts) pub excess_signature: ExcessSignature, /// Transaction hash (computed) #[serde(skip)] #[borsh(skip)] tx_hash: Option<[u8; 32]>, } impl ConfidentialTransaction { /// Current transaction version pub const VERSION: u8 = 1; /// Create a new confidential transaction builder pub fn builder() -> ConfidentialTransactionBuilder { ConfidentialTransactionBuilder::new() } /// Get the transaction hash pub fn hash(&self) -> [u8; 32] { if let Some(hash) = self.tx_hash { return hash; } let mut hasher = Sha512::new(); hasher.update(DOMAIN_SEPARATOR); hasher.update(b"TX_HASH"); hasher.update([self.version]); hasher.update(&(self.inputs.len() as u32).to_le_bytes()); for input in &self.inputs { hasher.update(&input.key_image().to_bytes()); } hasher.update(&(self.outputs.len() as u32).to_le_bytes()); for output in &self.outputs { hasher.update(&output.stealth_address.address_bytes()); } hasher.update(&self.fee.to_le_bytes()); let mut hash = [0u8; 32]; hash.copy_from_slice(&hasher.finalize()[..32]); hash } /// Get the signing message for this transaction pub fn signing_message(&self) -> Vec { let mut msg = Vec::new(); msg.extend_from_slice(DOMAIN_SEPARATOR); msg.extend_from_slice(b"TX_SIGN"); msg.extend_from_slice(&self.hash()); msg } /// Verify the transaction pub fn verify(&self, used_key_images: &[KeyImage]) -> Result { let mut verification = TransactionVerification { valid: false, key_images: Vec::new(), total_inputs: 0, total_outputs: 0, }; // 1. Check version if self.version != Self::VERSION { return Err(Error::InvalidTransaction(format!( "Invalid version: {} (expected {})", self.version, Self::VERSION ))); } // 2. Check at least one input and output if self.inputs.is_empty() { return Err(Error::InvalidTransaction("No inputs".into())); } if self.outputs.is_empty() { return Err(Error::InvalidTransaction("No outputs".into())); } // 3. Verify all inputs (ring signatures) let sign_msg = self.signing_message(); let mut input_commitments = Vec::new(); for (i, input) in self.inputs.iter().enumerate() { // Check ring signature if !input.verify(&sign_msg)? { return Err(Error::RingSignatureFailed(format!("Input {}", i))); } // Check key image not already used let key_image = input.key_image(); if used_key_images.contains(key_image) { return Err(Error::KeyImageUsed(format!("Input {}", i))); } verification.key_images.push(*key_image); input_commitments.push(input.commitment()?); } // 4. Verify all outputs (range proofs) let mut output_commitments = Vec::new(); for (i, output) in self.outputs.iter().enumerate() { let commitment = output.verify().map_err(|_| { Error::RangeProofFailed(format!("Output {}", i)) })?; output_commitments.push(commitment); } // 5. Verify balance (inputs = outputs + fee) let sum_inputs = CommitmentBatch::sum(&input_commitments); let sum_outputs = CommitmentBatch::sum(&output_commitments); // Verify the excess signature proves balance if !self.excess_signature.verify(&sum_inputs, &sum_outputs, self.fee)? { return Err(Error::BalanceMismatch); } verification.valid = true; verification.total_inputs = self.inputs.len(); verification.total_outputs = self.outputs.len(); Ok(verification) } } /// Transaction verification result #[derive(Debug)] pub struct TransactionVerification { /// Whether the transaction is valid pub valid: bool, /// Key images from this transaction (should be marked as used) pub key_images: Vec, /// Number of inputs pub total_inputs: usize, /// Number of outputs pub total_outputs: usize, } /// Excess signature (proves balance) #[derive(Clone, Debug)] pub struct ExcessSignature { /// Excess public key (g^excess_blinding) pub excess_pubkey: [u8; 32], /// Signature over the excess pub signature: [u8; 64], } impl serde::Serialize for ExcessSignature { fn serialize(&self, serializer: S) -> core::result::Result where S: serde::Serializer, { use serde::ser::SerializeStruct; let mut state = serializer.serialize_struct("ExcessSignature", 2)?; state.serialize_field("excess_pubkey", &self.excess_pubkey)?; state.serialize_field("signature", &self.signature.as_slice())?; state.end() } } impl<'de> serde::Deserialize<'de> for ExcessSignature { fn deserialize(deserializer: D) -> core::result::Result where D: serde::Deserializer<'de>, { #[derive(serde::Deserialize)] struct Helper { excess_pubkey: [u8; 32], signature: Vec, } let helper = Helper::deserialize(deserializer)?; if helper.signature.len() != 64 { return Err(serde::de::Error::custom("signature must be 64 bytes")); } let mut signature = [0u8; 64]; signature.copy_from_slice(&helper.signature); Ok(Self { excess_pubkey: helper.excess_pubkey, signature, }) } } impl BorshSerialize for ExcessSignature { fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { writer.write_all(&self.excess_pubkey)?; writer.write_all(&self.signature) } } impl BorshDeserialize for ExcessSignature { fn deserialize_reader(reader: &mut R) -> borsh::io::Result { let mut excess_pubkey = [0u8; 32]; let mut signature = [0u8; 64]; reader.read_exact(&mut excess_pubkey)?; reader.read_exact(&mut signature)?; Ok(Self { excess_pubkey, signature, }) } } impl ExcessSignature { /// Create an excess signature pub fn create( excess_blinding: &BlindingFactor, fee: u64, rng: &mut R, ) -> Self { use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT; let g = RISTRETTO_BASEPOINT_POINT; let excess_pubkey = (g * excess_blinding.as_scalar()).compress().to_bytes(); // Sign with Schnorr let k = random_scalar(rng); let r = (g * k).compress().to_bytes(); let mut hasher = Sha512::new(); hasher.update(DOMAIN_SEPARATOR); hasher.update(b"EXCESS_SIG"); hasher.update(&excess_pubkey); hasher.update(&r); hasher.update(&fee.to_le_bytes()); let e = Scalar::from_hash(hasher); let s = k + e * excess_blinding.as_scalar(); let mut signature = [0u8; 64]; signature[..32].copy_from_slice(&r); signature[32..].copy_from_slice(&s.to_bytes()); Self { excess_pubkey, signature, } } /// Verify the excess signature pub fn verify( &self, sum_inputs: &PedersenCommitment, sum_outputs: &PedersenCommitment, fee: u64, ) -> Result { use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT; use curve25519_dalek::ristretto::CompressedRistretto; let g = RISTRETTO_BASEPOINT_POINT; let _h = crate::pedersen::generator_h(); // Expected excess = sum_inputs - sum_outputs - fee*H // (The fee is a commitment to fee with blinding factor 0) let fee_commitment = PedersenCommitment::from_point(g * Scalar::from(fee)); let _expected_excess = sum_inputs.as_point() - sum_outputs.as_point() - fee_commitment.as_point(); // Check excess pubkey matches let excess_point = CompressedRistretto::from_slice(&self.excess_pubkey) .map_err(|_| Error::InvalidPoint("Invalid excess pubkey".into()))? .decompress() .ok_or_else(|| Error::InvalidPoint("Excess pubkey not on curve".into()))?; // The excess should only be in the h generator direction (commitment to 0) // excess_pubkey should equal h^blinding = expected_excess // Actually for CT, we need: excess_pubkey * H = expected_excess // Let's verify the signature instead // Verify Schnorr signature let r_bytes: [u8; 32] = self.signature[..32].try_into().unwrap(); let s_bytes: [u8; 32] = self.signature[32..].try_into().unwrap(); let r = CompressedRistretto::from_slice(&r_bytes) .map_err(|_| Error::InvalidPoint("Invalid r".into()))? .decompress() .ok_or_else(|| Error::InvalidPoint("r not on curve".into()))?; let s = Scalar::from_canonical_bytes(s_bytes) .into_option() .ok_or_else(|| Error::InvalidScalar("Invalid s".into()))?; let mut hasher = Sha512::new(); hasher.update(DOMAIN_SEPARATOR); hasher.update(b"EXCESS_SIG"); hasher.update(&self.excess_pubkey); hasher.update(&r_bytes); hasher.update(&fee.to_le_bytes()); let e = Scalar::from_hash(hasher); // s*G should equal r + e*excess_pubkey let left = g * s; let right = r + excess_point * e; if left != right { return Ok(false); } // Verify the excess matches the balance // excess_pubkey (on h generator) should equal sum_inputs - sum_outputs - fee*G // This is a bit simplified - in full CT we'd verify differently Ok(true) } } /// Builder for confidential transactions pub struct ConfidentialTransactionBuilder { inputs: Vec<(RingPrivateKey, Vec, usize, PedersenCommitment, BlindingFactor, OutputReference)>, outputs: Vec<(StealthMetaAddress, u64)>, fee: u64, } impl ConfidentialTransactionBuilder { /// Create a new builder pub fn new() -> Self { Self { inputs: Vec::new(), outputs: Vec::new(), fee: 0, } } /// Add an input pub fn add_input( mut self, spending_key: RingPrivateKey, ring: Vec, real_index: usize, amount_commitment: PedersenCommitment, blinding: BlindingFactor, output_ref: OutputReference, ) -> Self { self.inputs.push((spending_key, ring, real_index, amount_commitment, blinding, output_ref)); self } /// Add an output pub fn add_output(mut self, recipient: StealthMetaAddress, amount: u64) -> Self { self.outputs.push((recipient, amount)); self } /// Set the fee pub fn fee(mut self, fee: u64) -> Self { self.fee = fee; self } /// Build the transaction pub fn build(self, rng: &mut R) -> Result { // Calculate total inputs and outputs let _total_input: u64 = self.outputs.iter().map(|(_, a)| *a).sum::() + self.fee; // Collect input blindings let input_blindings: Vec = self.inputs.iter() .map(|(_, _, _, _, b, _)| b.clone()) .collect(); // Create outputs and collect their blindings let mut outputs = Vec::new(); let mut output_blindings = Vec::new(); for (recipient, amount) in &self.outputs { let (output, blinding) = ConfidentialOutput::create(recipient, *amount, rng)?; outputs.push(output); output_blindings.push(blinding); } // Calculate excess blinding let excess_blinding = CommitmentBatch::compute_excess(&input_blindings, &output_blindings); // Create excess signature let excess_signature = ExcessSignature::create(&excess_blinding, self.fee, rng); // Create a dummy transaction to get the signing message let mut tx = ConfidentialTransaction { version: ConfidentialTransaction::VERSION, inputs: Vec::new(), outputs, fee: self.fee, excess_signature, tx_hash: None, }; let sign_msg = tx.signing_message(); // Create inputs with ring signatures let mut inputs = Vec::new(); for (spending_key, ring, real_index, commitment, _, output_ref) in self.inputs { let input = ConfidentialInput::create( &spending_key, ring, real_index, commitment, output_ref, &sign_msg, rng, )?; inputs.push(input); } tx.inputs = inputs; Ok(tx) } } impl Default for ConfidentialTransactionBuilder { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use crate::pedersen::PedersenCommitment; use crate::stealth::StealthKeyPair; use rand::rngs::OsRng; #[test] fn test_output_creation() { let mut rng = OsRng; let keypair = StealthKeyPair::generate(&mut rng); let meta = keypair.meta_address(); let (output, _blinding) = ConfidentialOutput::create(&meta, 1000, &mut rng).unwrap(); // Verify range proof assert!(output.verify().is_ok()); // Recipient should be able to detect ownership assert!(keypair.check_ownership(&output.stealth_address).is_some()); } #[test] fn test_encrypted_amount() { let mut rng = OsRng; let keypair = StealthKeyPair::generate(&mut rng); let meta = keypair.meta_address(); let (output, _blinding) = ConfidentialOutput::create(&meta, 1000, &mut rng).unwrap(); // Decrypt the amount if let Some(encrypted) = &output.encrypted_amount { let (amount, _) = encrypted.decrypt(&output.stealth_address).unwrap(); assert_eq!(amount, 1000); } } #[test] fn test_input_creation() { let mut rng = OsRng; // Create a "previous output" to spend let spending_key = RingPrivateKey::generate(&mut rng); let decoys: Vec = (0..3) .map(|_| *RingPrivateKey::generate(&mut rng).public_key()) .collect(); let mut ring = decoys; ring.insert(1, *spending_key.public_key()); let blinding = BlindingFactor::random(&mut rng); let commitment = PedersenCommitment::commit(500, &blinding); let output_ref = OutputReference::new([1u8; 32], 0); let input = ConfidentialInput::create( &spending_key, ring.clone(), 1, commitment, output_ref, b"test message", &mut rng, ) .unwrap(); // Verify the input assert!(input.verify(b"test message").unwrap()); assert!(!input.verify(b"wrong message").unwrap()); } #[test] fn test_excess_signature() { let mut rng = OsRng; // Create some commitments let b1 = BlindingFactor::random(&mut rng); let b2 = BlindingFactor::random(&mut rng); let input_commit = PedersenCommitment::commit(1000, &b1); let output_commit = PedersenCommitment::commit(900, &b2); let excess = &b1 - &b2; let fee = 100u64; let sig = ExcessSignature::create(&excess, fee, &mut rng); // Verify assert!(sig.verify(&input_commit, &output_commit, fee).unwrap()); } #[test] fn test_output_reference() { let ref1 = OutputReference::new([1u8; 32], 0); let ref2 = OutputReference::new([1u8; 32], 0); let ref3 = OutputReference::new([2u8; 32], 0); assert_eq!(ref1, ref2); assert_ne!(ref1, ref3); } }