use anyhow::{Context, Result}; use curve25519_dalek::constants::ED25519_BASEPOINT_POINT; use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint}; use curve25519_dalek::scalar::Scalar; use hash_edwards_to_edwards::hash_point_to_point; use itertools::Itertools; use monero::blockdata::transaction::{KeyImage, SubField, TxOutTarget}; use monero::cryptonote::hash::Hashable; use monero::cryptonote::onetime_key::KeyGenerator; use monero::util::key::H; use monero::util::ringct::{CtKey, EcdhInfo, Key, RctSig, RctSigBase, RctSigPrunable, RctType}; use monero::{ Address, KeyPair, OwnedTxOut, PrivateKey, PublicKey, Transaction, TransactionPrefix, TxIn, TxOut, VarInt, }; use monero_rpc::monerod; use monero_rpc::monerod::{GetBlockResponse, GetOutputsOut, MonerodRpc as _}; use rand::{CryptoRng, RngCore}; use std::convert::TryInto; use std::iter; pub struct ConfidentialTransactionBuilder { prefix: TransactionPrefix, base: RctSigBase, prunable: RctSigPrunable, blinding_factors: Vec, amounts: Vec, decoy_inputs: [DecoyInput; 10], actual_signing_key: Scalar, real_commitment_blinder: Scalar, signing_pk: EdwardsPoint, H_p_pk: EdwardsPoint, input_commitment: EdwardsPoint, spend_amount: u64, global_output_index: u64, } impl ConfidentialTransactionBuilder { pub fn new( input_to_spend: OwnedTxOut<'_>, global_output_index: u64, decoy_inputs: [DecoyInput; 10], keys: KeyPair, ) -> Self { let prefix = TransactionPrefix { version: VarInt(2), ..TransactionPrefix::default() }; let actual_signing_key = input_to_spend.recover_key(&keys).scalar; let signing_pk = actual_signing_key * ED25519_BASEPOINT_POINT; Self { prefix, base: RctSigBase { rct_type: RctType::Clsag, txn_fee: VarInt(0), pseudo_outs: vec![], ecdh_info: vec![], out_pk: vec![], }, prunable: RctSigPrunable { range_sigs: vec![], bulletproofs: vec![], MGs: vec![], Clsags: vec![], pseudo_outs: vec![], }, blinding_factors: vec![], amounts: vec![], decoy_inputs, actual_signing_key, signing_pk, H_p_pk: hash_point_to_point(signing_pk), input_commitment: input_to_spend.commitment().unwrap(), // TODO: Error handling spend_amount: input_to_spend.amount().unwrap(), // TODO: Error handling, global_output_index, real_commitment_blinder: input_to_spend.blinding_factor().unwrap(), // TODO: Error handling } } pub fn with_output( mut self, to: Address, amount: u64, rng: &mut (impl RngCore + CryptoRng), ) -> Self { let next_index = self.prefix.outputs.len(); let ecdh_key = PrivateKey::random(rng); let (ecdh_info, blinding_factor) = EcdhInfo::new_bulletproof(amount, ecdh_key.scalar); let out = TxOut { amount: VarInt(0), target: TxOutTarget::ToKey { key: KeyGenerator::from_random(to.public_view, to.public_spend, ecdh_key) .one_time_key(dbg!(next_index)), }, }; self.prefix.outputs.push(out); self.prefix .extra .0 .push(SubField::TxPublicKey(PublicKey::from_private_key( &ecdh_key, ))); self.base.ecdh_info.push(ecdh_info); self.blinding_factors.push(blinding_factor); self.amounts.push(amount); // sanity checks debug_assert_eq!(self.prefix.outputs.len(), self.prefix.extra.0.len()); debug_assert_eq!(self.prefix.outputs.len(), self.blinding_factors.len()); debug_assert_eq!(self.prefix.outputs.len(), self.amounts.len()); debug_assert_eq!(self.prefix.outputs.len(), self.base.ecdh_info.len()); self } fn compute_fee(&self) -> u64 { self.spend_amount - self.amounts.iter().sum::() } fn compute_pseudo_out(&mut self, commitments: Vec) -> EdwardsPoint { let sum_commitments = commitments .iter() .map(|p| p * Scalar::from(8u8)) // TODO: Should this happen inside the bulletproof module? => yes .sum::(); let fee = self.compute_fee(); // TODO: Don't mutate in here self.base.txn_fee = VarInt(fee); self.base.out_pk = commitments .iter() .map(|p| p * Scalar::from(8u8)) .map(|p| CtKey { mask: Key { key: p.compress().0, }, }) .collect(); let fee_key = Scalar::from(fee) * H.point.decompress().unwrap(); let pseudo_out = fee_key + sum_commitments; self.prunable.pseudo_outs = vec![Key { key: pseudo_out.compress().0, }]; pseudo_out } pub fn build(mut self, rng: &mut (impl RngCore + CryptoRng)) -> Transaction { // 0. add dummy output if necessary // 1. compute fee // 2. make bullet-proof // 3. sign // TODO: move to a function let (bulletproof, output_commitments) = monero::make_bulletproof( rng, self.amounts.as_slice(), self.blinding_factors.as_slice(), ) .unwrap(); self.prunable.bulletproofs = vec![bulletproof]; // TODO: move to ctor let (key_offsets, ring, commitment_ring) = self .decoy_inputs .iter() .copied() .map( |DecoyInput { global_output_index, key, commitment, }| { (VarInt(global_output_index), key, commitment) }, ) .chain(std::iter::once(( VarInt(self.global_output_index), self.signing_pk, self.input_commitment, ))) .sorted_by(|(a, ..), (b, ..)| Ord::cmp(a, b)) .fold( (Vec::new(), Vec::new(), Vec::new()), |(mut key_offsets, mut ring, mut commitment_ring), (key_offset, key, commitment)| { key_offsets.push(key_offset); ring.push(key); commitment_ring.push(commitment); (key_offsets, ring, commitment_ring) }, ); let ring: [EdwardsPoint; 11] = ring.try_into().unwrap(); let commitment_ring = commitment_ring.try_into().unwrap(); let (signing_index, _) = ring .iter() .find_position(|key| **key == self.signing_pk) .unwrap(); let relative_key_offsets = to_relative_offsets(&key_offsets); let I = self.actual_signing_key * self.H_p_pk; self.prefix.inputs = vec![TxIn::ToKey { amount: VarInt(0), key_offsets: relative_key_offsets, k_image: KeyImage { image: monero::cryptonote::hash::Hash(I.compress().to_bytes()), }, }]; let output_commitments = output_commitments .into_iter() .map(|p| p.decompress().unwrap()) .collect(); // TODO: Return EdwardsPoints from bulletproof lib let pseudo_out = self.compute_pseudo_out(output_commitments); // TODO: either mutate or return let mut transaction = Transaction { prefix: self.prefix, rct_signatures: RctSig { sig: Some(self.base), p: Some(self.prunable), }, ..Transaction::default() }; let alpha = Scalar::random(rng); let fake_responses = random_array(|| Scalar::random(rng)); let message = transaction.signature_hash().unwrap(); let sig = monero::clsag::sign( message.as_fixed_bytes(), self.actual_signing_key, signing_index, self.H_p_pk, alpha, &ring, &commitment_ring, fake_responses, self.real_commitment_blinder - (self.blinding_factors.iter().sum::()), pseudo_out, alpha * ED25519_BASEPOINT_POINT, alpha * self.H_p_pk, I, ); transaction.rct_signatures.p.as_mut().unwrap().Clsags = vec![sig]; dbg!(transaction) } } #[derive(Debug, Copy, Clone)] pub struct DecoyInput { global_output_index: u64, key: EdwardsPoint, commitment: EdwardsPoint, } fn to_relative_offsets(offsets: &[VarInt]) -> Vec { let vals = offsets.iter(); let next_vals = offsets.iter().skip(1); let diffs = vals .zip(next_vals) .map(|(cur, next)| VarInt(next.0 - cur.0)); iter::once(offsets[0].clone()).chain(diffs).collect() } fn random_array(rng: impl FnMut() -> T) -> [T; N] { let mut ring = [T::default(); N]; ring[..].fill_with(rng); ring } #[async_trait::async_trait] pub trait MonerodClientExt { async fn calculate_key_offset_boundaries(&self) -> Result<(VarInt, VarInt)>; async fn fetch_decoy_inputs(&self, indices: [u64; 10]) -> Result<[DecoyInput; 10]>; } #[async_trait::async_trait] impl MonerodClientExt for monerod::Client { /// Chooses 10 random key offsets for use within a new confidential /// transactions. /// /// Choosing these offsets randomly is not ideal for privacy, instead they /// should be chosen in a way that mimics a real spending pattern as much as /// possible. async fn calculate_key_offset_boundaries(&self) -> Result<(VarInt, VarInt)> { let latest_block = self.get_block_count().await?; let latest_spendable_block = latest_block.count - 100; let block: GetBlockResponse = self.get_block(latest_spendable_block).await?; let tx_hash = block .blob .tx_hashes .first() .copied() .unwrap_or_else(|| block.blob.miner_tx.hash()); let indices = self.get_o_indexes(tx_hash).await?; let last_index = indices .o_indexes .into_iter() .max() .context("Expected at least one output index")?; // let oldest_index = last_index - (last_index / 100) * 40; // oldest index must be within last 40% TODO: CONFIRM THIS Ok((VarInt(0), VarInt(last_index))) } async fn fetch_decoy_inputs(&self, indices: [u64; 10]) -> Result<[DecoyInput; 10]> { let response = self .get_outs( indices .iter() .map(|offset| GetOutputsOut { amount: 0, index: *offset, }) .collect(), ) .await?; let inputs = response .outs .into_iter() .zip(indices.iter()) .map(|(out_key, index)| { DecoyInput { global_output_index: *index, key: out_key.key.point.decompress().unwrap(), // TODO: should decompress on deserialization commitment: CompressedEdwardsY(out_key.mask.key).decompress().unwrap(), } }) .collect::>() .try_into() .expect("exactly 10 elements guaranteed through type-safety of array"); Ok(inputs) } } #[cfg(test)] mod tests { use super::*; use monero_harness::image::Monerod; use monero_rpc::monerod::Client; use testcontainers::clients::Cli; use testcontainers::Docker; #[test] fn calculate_relative_key_offsets() { let key_offsets = [ VarInt(78), VarInt(81), VarInt(91), VarInt(91), VarInt(96), VarInt(98), VarInt(101), VarInt(112), VarInt(113), VarInt(114), VarInt(117), ]; let relative_offsets = to_relative_offsets(&key_offsets); assert_eq!( &relative_offsets, &[ VarInt(78), VarInt(3), VarInt(10), VarInt(0), VarInt(5), VarInt(2), VarInt(3), VarInt(11), VarInt(1), VarInt(1), VarInt(3), ] ) } #[tokio::test] async fn get_outs_for_key_offsets() { let cli = Cli::default(); let container = cli.run(Monerod::default()); let rpc_client = Client::localhost(container.get_host_port(18081).unwrap()).unwrap(); rpc_client.generateblocks(150, "498AVruCDWgP9Az9LjMm89VWjrBrSZ2W2K3HFBiyzzrRjUJWUcCVxvY1iitfuKoek2FdX6MKGAD9Qb1G1P8QgR5jPmmt3Vj".to_owned()).await.unwrap(); // let wallet = Wallet { // client: rpc_client.clone(), // key: todo!(), // }; // // let (lower, upper) = wallet.calculate_key_offset_boundaries().await.unwrap(); todo!("fix"); // let result = rpc_client // .get_outs( // key_offsets // .to_vec() // .into_iter() // .map(|varint| GetOutputsOut { // amount: 0, // index: varint.0, // }) // .collect(), // ) // .await // .unwrap(); // assert_eq!(result.outs.len(), 10); } }