diff --git a/Cargo.lock b/Cargo.lock index 9de934274e3..65875aca67f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3511,6 +3511,7 @@ dependencies = [ "itertools 0.10.5", "libsqlite3-sys", "log", + "merlin", "prost", "rand", "serde", @@ -3518,6 +3519,7 @@ dependencies = [ "sha2 0.10.8", "strum", "strum_macros", + "tari_bulletproofs_plus", "tari_common", "tari_common_sqlite", "tari_common_types", diff --git a/applications/minotari_app_utilities/src/utilities.rs b/applications/minotari_app_utilities/src/utilities.rs index 9201a17a5e4..7d5c7caa411 100644 --- a/applications/minotari_app_utilities/src/utilities.rs +++ b/applications/minotari_app_utilities/src/utilities.rs @@ -97,6 +97,12 @@ impl From for PublicKey { } } +impl UniPublicKey { + pub fn as_public_key(&self) -> &PublicKey { + &self.0 + } +} + #[derive(Debug)] pub enum UniNodeId { PublicKey(PublicKey), diff --git a/applications/minotari_console_wallet/src/automation/commands.rs b/applications/minotari_console_wallet/src/automation/commands.rs index a912c612add..7ef19090a3b 100644 --- a/applications/minotari_console_wallet/src/automation/commands.rs +++ b/applications/minotari_console_wallet/src/automation/commands.rs @@ -107,6 +107,8 @@ pub enum WalletCommand { MintTokens, CreateInitialCheckpoint, RevalidateWalletDb, + CreateCommitmentProof, + VerifyCommitmentProof, } #[derive(Debug)] @@ -1049,6 +1051,72 @@ pub async fn command_runner( }, Err(err) => eprintln!("Error generating certificates: {}", err), }, + CreateCommitmentProof(args) => match output_service.get_unspent_outputs().await { + Ok(utxos) => { + // Parse into outputs and commitments + let utxos: Vec<(WalletOutput, Commitment)> = utxos + .into_iter() + .map(|v| (v.wallet_output, v.commitment)) + .filter(|(_, c)| c.as_public_key() == args.commitment.as_public_key()) + .collect(); + + // Make sure we have a single unspent output corresponding to the requested commitment + let count = utxos.len(); + if count == 0 { + eprintln!("No unspent UTXOs match this commitment"); + continue; + } + if count > 1 { + eprintln!("Multiple unspent UTXOs match this commitment"); + continue; + } + + // Try to generate the commitment proof + match output_service + .create_commitment_proof(utxos[0].1.clone(), args.message, args.minimum_value) + .await + { + Ok(proof) => { + println!("Commitment proof: {}", proof.to_hex()); + }, + Err(e) => eprintln!("CreateCommitmentProof error! {}", e), + } + }, + Err(e) => eprintln!("CreateCommitmentProof error! {}", e), + }, + VerifyCommitmentProof(args) => match output_service.get_unspent_outputs().await { + Ok(utxos) => { + // Parse into outputs and commitments + let utxos: Vec<(WalletOutput, Commitment)> = utxos + .into_iter() + .map(|v| (v.wallet_output, v.commitment)) + .filter(|(_, c)| c.as_public_key() == args.commitment.as_public_key()) + .collect(); + + // Make sure we have a single unspent output corresponding to the requested commitment + let count = utxos.len(); + if count == 0 { + eprintln!("No unspent UTXOs match this commitment"); + continue; + } + if count > 1 { + eprintln!("Multiple unspent UTXOs match this commitment"); + continue; + } + + // Try to verify the commitment proof + match output_service + .verify_commitment_proof(utxos[0].1.clone(), args.message, args.minimum_value, args.proof) + .await + { + Ok(()) => { + println!("Commitment proof verified!"); + }, + Err(e) => eprintln!("VerifyCommitmentProof error! {}", e), + } + }, + Err(e) => eprintln!("VerifyCommitmentProof error! {}", e), + }, } } diff --git a/applications/minotari_console_wallet/src/cli.rs b/applications/minotari_console_wallet/src/cli.rs index 496347181a7..cc6bd98aacd 100644 --- a/applications/minotari_console_wallet/src/cli.rs +++ b/applications/minotari_console_wallet/src/cli.rs @@ -136,6 +136,8 @@ pub enum CliCommands { RevalidateWalletDb, RegisterValidatorNode(RegisterValidatorNodeArgs), CreateTlsCerts, + CreateCommitmentProof(CreateCommitmentProofArgs), + VerifyCommitmentProof(VerifyCommitmentProofArgs), } #[derive(Debug, Args, Clone)] @@ -305,3 +307,18 @@ pub struct RegisterValidatorNodeArgs { #[clap(short, long, default_value = "Registering VN")] pub message: String, } + +#[derive(Debug, Args, Clone)] +pub struct CreateCommitmentProofArgs { + pub commitment: UniPublicKey, + pub message: String, + pub minimum_value: Option, +} + +#[derive(Debug, Args, Clone)] +pub struct VerifyCommitmentProofArgs { + pub commitment: UniPublicKey, + pub message: String, + pub minimum_value: Option, + pub proof: String, +} diff --git a/applications/minotari_console_wallet/src/wallet_modes.rs b/applications/minotari_console_wallet/src/wallet_modes.rs index 70fe2026524..7c1cb346e04 100644 --- a/applications/minotari_console_wallet/src/wallet_modes.rs +++ b/applications/minotari_console_wallet/src/wallet_modes.rs @@ -552,6 +552,8 @@ mod test { CliCommands::RevalidateWalletDb => {}, CliCommands::RegisterValidatorNode(_) => {}, CliCommands::CreateTlsCerts => {}, + CliCommands::CreateCommitmentProof(_) => {}, + CliCommands::VerifyCommitmentProof(_) => {}, } } assert!( diff --git a/base_layer/wallet/Cargo.toml b/base_layer/wallet/Cargo.toml index f97f3a76fad..248e04f42d0 100644 --- a/base_layer/wallet/Cargo.toml +++ b/base_layer/wallet/Cargo.toml @@ -33,6 +33,7 @@ argon2 = "0.4.1" bincode = "1.3.1" blake2 = "0.10" borsh = "1.2" +tari_bulletproofs_plus = "0.4" sha2 = "0.10" chrono = { version = "0.4.19", default-features = false, features = ["serde"] } derivative = "2.2.0" @@ -43,6 +44,7 @@ fs2 = "0.4.0" futures = { version = "^0.3.1", features = ["compat", "std"] } libsqlite3-sys = { version = "0.25.1", features = ["bundled"], optional = true } log = "0.4.6" +merlin = "3" rand = "0.8" serde = { version = "1.0.89", features = ["derive"] } serde_json = "1.0.39" diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index 54caae3e114..e6c2a0f25c3 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -118,6 +118,8 @@ pub enum OutputManagerRequest { CreateClaimShaAtomicSwapTransaction(HashOutput, PublicKey, MicroMinotari), CreateHtlcRefundTransaction(HashOutput, MicroMinotari), GetOutputInfoByTxId(TxId), + CreateCommitmentProof(Commitment, String, Option), + VerifyCommitmentProof(Commitment, String, Option, String), } impl fmt::Display for OutputManagerRequest { @@ -200,8 +202,29 @@ impl fmt::Display for OutputManagerRequest { "CreateHtlcRefundTransaction(output hash: {}, , fee_per_gram: {} )", output, fee_per_gram, ), - GetOutputInfoByTxId(t) => write!(f, "GetOutputInfoByTxId: {}", t), + CreateCommitmentProof(commitment, message, minimum_value) => write!( + f, + "CreateCommitmentProof(commitment: {}, message: {}, minimum_value: {})", + commitment.to_hex(), + message, + if let Some(minimum_value) = minimum_value { + *minimum_value + } else { + MicroMinotari::zero() + }, + ), + VerifyCommitmentProof(commitment, message, minimum_value, _) => write!( + f, + "CreateCommitmentProof(commitment: {}, message: {}, minimum_value: {})", + commitment.to_hex(), + message, + if let Some(minimum_value) = minimum_value { + *minimum_value + } else { + MicroMinotari::zero() + }, + ), } } } @@ -238,6 +261,8 @@ pub enum OutputManagerResponse { ClaimHtlcTransaction((TxId, MicroMinotari, MicroMinotari, Transaction)), OutputInfoByTxId(OutputInfoByTxId), CoinPreview((Vec, MicroMinotari)), + CommitmentProofCreated(Vec), + CommitmentProofVerified, } pub type OutputManagerEventSender = broadcast::Sender>; @@ -391,6 +416,48 @@ impl OutputManagerHandle { } } + pub async fn create_commitment_proof( + &mut self, + commitment: Commitment, + message: String, + minimum_value: Option, + ) -> Result, OutputManagerError> { + match self + .handle + .call(OutputManagerRequest::CreateCommitmentProof( + commitment, + message, + minimum_value, + )) + .await?? + { + OutputManagerResponse::CommitmentProofCreated(bytes) => Ok(bytes), + _ => Err(OutputManagerError::UnexpectedApiResponse), + } + } + + pub async fn verify_commitment_proof( + &mut self, + commitment: Commitment, + message: String, + minimum_value: Option, + proof: String, + ) -> Result<(), OutputManagerError> { + match self + .handle + .call(OutputManagerRequest::VerifyCommitmentProof( + commitment, + message, + minimum_value, + proof, + )) + .await?? + { + OutputManagerResponse::CommitmentProofVerified => Ok(()), + _ => Err(OutputManagerError::UnexpectedApiResponse), + } + } + pub async fn get_balance(&mut self) -> Result { match self.handle.call(OutputManagerRequest::GetBalance).await?? { OutputManagerResponse::Balance(b) => Ok(b), diff --git a/base_layer/wallet/src/output_manager_service/mod.rs b/base_layer/wallet/src/output_manager_service/mod.rs index ef26e0d5a2b..2aca90cf2f3 100644 --- a/base_layer/wallet/src/output_manager_service/mod.rs +++ b/base_layer/wallet/src/output_manager_service/mod.rs @@ -39,7 +39,7 @@ use futures::future; use log::*; use tari_core::{ consensus::NetworkConsensus, - transactions::{key_manager::TransactionKeyManagerInterface, CryptoFactories}, + transactions::{key_manager::SecretTransactionKeyManagerInterface, CryptoFactories}, }; use tari_service_framework::{ async_trait, @@ -103,7 +103,7 @@ where T: OutputManagerBackend + 'static impl ServiceInitializer for OutputManagerServiceInitializer where T: OutputManagerBackend + 'static, - TKeyManagerInterface: TransactionKeyManagerInterface, + TKeyManagerInterface: SecretTransactionKeyManagerInterface, { async fn initialize(&mut self, context: ServiceInitializerContext) -> Result<(), ServiceInitializationError> { let (sender, receiver) = reply_channel::unbounded(); diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index a24deb7df62..e6fa77a6c8c 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -28,7 +28,7 @@ use log::*; use rand::{rngs::OsRng, RngCore}; use tari_common_types::{ transaction::TxId, - types::{BlockHash, Commitment, HashOutput, PrivateKey, PublicKey}, + types::{BlockHash, Commitment, HashOutput, PrivateKey, PublicKey, RANGE_PROOF_BIT_LENGTH}, }; use tari_comms::types::CommsDHKE; use tari_core::{ @@ -39,7 +39,7 @@ use tari_core::{ proto::base_node::FetchMatchingUtxos, transactions::{ fee::Fee, - key_manager::{TariKeyId, TransactionKeyManagerBranch, TransactionKeyManagerInterface}, + key_manager::{SecretTransactionKeyManagerInterface, TariKeyId, TransactionKeyManagerBranch}, tari_amount::MicroMinotari, transaction_components::{ EncryptedData, @@ -62,7 +62,7 @@ use tari_crypto::keys::SecretKey; use tari_script::{inputs, script, ExecutionStack, Opcode, TariScript}; use tari_service_framework::reply_channel; use tari_shutdown::ShutdownSignal; -use tari_utilities::{hex::Hex, ByteArray}; +use tari_utilities::{hex::{from_hex, Hex}, ByteArray}; use tokio::{sync::Mutex, time::Instant}; use crate::{ @@ -113,7 +113,7 @@ impl where TBackend: OutputManagerBackend + 'static, TWalletConnectivity: WalletConnectivityInterface, - TKeyManagerInterface: TransactionKeyManagerInterface, + TKeyManagerInterface: SecretTransactionKeyManagerInterface, { pub async fn new( config: OutputManagerServiceConfig, @@ -411,9 +411,124 @@ where let output_statuses_by_tx_id = self.get_output_info_by_tx_id(tx_id)?; Ok(OutputManagerResponse::OutputInfoByTxId(output_statuses_by_tx_id)) }, + OutputManagerRequest::CreateCommitmentProof(commitment, message, minimum_value) => self + .create_commitment_proof(commitment, message, minimum_value) + .await + .map(OutputManagerResponse::CommitmentProofCreated), + OutputManagerRequest::VerifyCommitmentProof(commitment, message, minimum_value, proof) => self + .verify_commitment_proof(commitment, message, minimum_value, proof) + .map(|_| OutputManagerResponse::CommitmentProofVerified), } } + async fn create_commitment_proof( + &self, + commitment: Commitment, + message: String, + minimum_value: Option, + ) -> Result, OutputManagerError> { + use tari_bulletproofs_plus::*; + + // Get the output details + let output: WalletOutput = self.resources.db.fetch_by_commitment(commitment.clone())?.into(); + let mask = self + .resources + .key_manager + .get_private_key(&output.spending_key_id) + .await?; + + // Generate the parameters + let params = range_parameters::RangeParameters::init( + RANGE_PROOF_BIT_LENGTH, + 1, + ristretto::create_pedersen_gens_with_extension_degree( + generators::pedersen_gens::ExtensionDegree::DefaultPedersen, + ), + ) + .map_err(|_| OutputManagerError::RangeProofError("Unable to create commitment proof".to_string()))?; + + // Generate the witness + let opening = commitment_opening::CommitmentOpening::new(output.value.as_u64(), vec![mask.into()]); + let witness = range_witness::RangeWitness::init(vec![opening]) + .map_err(|_| OutputManagerError::RangeProofError("Unable to create commitment proof".to_string()))?; + + // Generate the statement + let statement = range_statement::RangeStatement::init( + params, + vec![commitment.as_public_key().point()], + vec![minimum_value.map(|v| v.as_u64())], + None, + ) + .map_err(|_| OutputManagerError::RangeProofError("Unable to create commitment proof".to_string()))?; + + // Start the transcript + let mut transcript = merlin::Transcript::new(b"Tari commitment proof"); + transcript.append_u64(b"version", 1); + transcript.append_message(b"message", message.as_bytes()); + + // Generate the proof + let proof = ristretto::RistrettoRangeProof::prove(&mut transcript, &statement, &witness) + .map_err(|_| OutputManagerError::RangeProofError("Unable to create commitment proof".to_string()))?; + + Ok(proof.to_bytes()) + } + + fn verify_commitment_proof( + &self, + commitment: Commitment, + message: String, + minimum_value: Option, + proof: String, + ) -> Result<(), OutputManagerError> { + use tari_bulletproofs_plus::*; + + // Try to decode the proof from hex to bytes + let bytes = from_hex(&proof) + .map_err(|_| OutputManagerError::RangeProofError("Unable to parse commitment proof".to_string()))?; + + // Generate the parameters + let params = range_parameters::RangeParameters::init( + RANGE_PROOF_BIT_LENGTH, + 1, + ristretto::create_pedersen_gens_with_extension_degree( + generators::pedersen_gens::ExtensionDegree::DefaultPedersen, + ), + ) + .map_err(|_| { + OutputManagerError::RangeProofError("Unable to set up commitment proof verification".to_string()) + })?; + + // Generate the statement + let statement = range_statement::RangeStatement::init( + params, + vec![commitment.as_public_key().point()], + vec![minimum_value.map(|v| v.as_u64())], + None, + ) + .map_err(|_| { + OutputManagerError::RangeProofError("Unable to set up commitment proof verification".to_string()) + })?; + + // Start the transcript + let mut transcript = merlin::Transcript::new(b"Tari commitment proof"); + transcript.append_u64(b"version", 1); + transcript.append_message(b"message", message.as_bytes()); + + // Try to parse the proof + let proof = ristretto::RistrettoRangeProof::from_bytes(&bytes) + .map_err(|_| OutputManagerError::RangeProofError("Unable to parse commitment proof".to_string()))?; + + // Verify the proof + ristretto::RistrettoRangeProof::verify_batch( + &mut [transcript], + &[statement], + &[proof], + range_proof::VerifyAction::VerifyOnly, + ) + .map_err(|_| OutputManagerError::RangeProofError("Commitment proof verification failed".to_string())) + .and(Ok(())) + } + fn get_output_info_by_tx_id(&self, tx_id: TxId) -> Result { let outputs = self.resources.db.fetch_outputs_by_tx_id(tx_id)?; let statuses = outputs.clone().into_iter().map(|uo| uo.status).collect();