From 54f290cdf27669e50840221d5ffb86cf6f557dc1 Mon Sep 17 00:00:00 2001 From: Aaron Feickert <66188213+AaronFeickert@users.noreply.github.com> Date: Sun, 11 Aug 2024 19:11:31 -0500 Subject: [PATCH] Commitment proofs --- Cargo.lock | 2 + .../minotari_app_utilities/src/utilities.rs | 6 + .../src/automation/commands.rs | 70 ++++++++++ .../minotari_console_wallet/src/cli.rs | 17 +++ .../src/wallet_modes.rs | 4 +- base_layer/wallet/Cargo.toml | 2 + .../src/output_manager_service/handle.rs | 68 ++++++++++ .../src/output_manager_service/service.rs | 126 +++++++++++++++++- 8 files changed, 290 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8b4163b9e..7d1886e066 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3550,6 +3550,7 @@ dependencies = [ "itertools 0.10.5", "libsqlite3-sys", "log", + "merlin", "prost", "rand", "serde", @@ -3557,6 +3558,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 40b6eb3bc2..9d05a1164b 100644 --- a/applications/minotari_app_utilities/src/utilities.rs +++ b/applications/minotari_app_utilities/src/utilities.rs @@ -110,6 +110,12 @@ pub enum UniIdError { Nonconvertible, } +impl UniPublicKey { + pub fn as_public_key(&self) -> &PublicKey { + &self.0 + } +} + impl FromStr for UniNodeId { type Err = UniIdError; diff --git a/applications/minotari_console_wallet/src/automation/commands.rs b/applications/minotari_console_wallet/src/automation/commands.rs index 0b266077e4..934fb05701 100644 --- a/applications/minotari_console_wallet/src/automation/commands.rs +++ b/applications/minotari_console_wallet/src/automation/commands.rs @@ -1816,6 +1816,76 @@ pub async fn command_runner( println!("Spend key: {}", spend_key_hex); } }, + 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 outputs match this commitment"); + continue; + } + if count > 1 { + eprintln!("Multiple unspent outputs match this commitment"); + continue; + } + + // Try to generate the requested 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 outputs match this commitment"); + continue; + } + if count > 1 { + eprintln!("Multiple unspent outputs match this commitment"); + continue; + } + + // Try to verify the requested 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!("VerifyProof 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 6d973d709b..4e9749d731 100644 --- a/applications/minotari_console_wallet/src/cli.rs +++ b/applications/minotari_console_wallet/src/cli.rs @@ -149,6 +149,8 @@ pub enum CliCommands { CreateTlsCerts, Sync(SyncArgs), ExportViewKeyAndSpendKey(ExportViewKeyAndSpendKeyArgs), + CreateCommitmentProof(CreateCommitmentProofArgs), + VerifyCommitmentProof(VerifyCommitmentProofArgs), } #[derive(Debug, Args, Clone)] @@ -380,3 +382,18 @@ pub struct SyncArgs { #[clap(short, long, default_value = "0")] pub sync_to_height: u64, } + +#[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 edffc487a2..ef8d00352b 100644 --- a/applications/minotari_console_wallet/src/wallet_modes.rs +++ b/applications/minotari_console_wallet/src/wallet_modes.rs @@ -530,7 +530,7 @@ async fn run_grpc( mod test { use std::path::Path; - use crate::{cli::CliCommands, wallet_modes::parse_command_file}; + use crate::{cli::CliCommands, wallet_modes::parse_command_file, Cli}; #[test] #[allow(clippy::too_many_lines)] @@ -640,6 +640,8 @@ mod test { CliCommands::PreMineSpendBackupUtxo(_) => {}, CliCommands::Sync(_) => {}, CliCommands::ExportViewKeyAndSpendKey(_) => {}, + CliCommands::CreateCommitmentProof(_) => {}, + CliCommands::VerifyCommitmentProof(_) => {}, } } assert!( diff --git a/base_layer/wallet/Cargo.toml b/base_layer/wallet/Cargo.toml index 54c862a099..4ee1237e02 100644 --- a/base_layer/wallet/Cargo.toml +++ b/base_layer/wallet/Cargo.toml @@ -55,6 +55,8 @@ prost = "0.11.9" itertools = "0.10.3" chacha20poly1305 = "0.10.1" zeroize = "1" +tari_bulletproofs_plus = "0.4.0" +merlin = "3.0.0" [build-dependencies] tari_common = { path = "../../common", features = ["build", "static-application-info"], version = "1.1.0-pre.2" } diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index 5c722b23e5..7512740edd 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -139,6 +139,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 { @@ -247,6 +249,28 @@ impl fmt::Display for OutputManagerRequest { ), 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, + "VerifyCommitmentProof(commitment: {}, message: {}, minimum_value: {}, proof: (not shown))", + commitment.to_hex(), + message, + if let Some(minimum_value) = minimum_value { + *minimum_value + } else { + MicroMinotari::zero() + }, + ), } } } @@ -299,6 +323,8 @@ pub enum OutputManagerResponse { ClaimHtlcTransaction((TxId, MicroMinotari, MicroMinotari, Transaction)), OutputInfoByTxId(OutputInfoByTxId), CoinPreview((Vec, MicroMinotari)), + CommitmentProofCreated(Vec), + CommitmentProofVerified(()), } pub type OutputManagerEventSender = broadcast::Sender>; @@ -911,4 +937,46 @@ impl OutputManagerHandle { _ => Err(OutputManagerError::UnexpectedApiResponse), } } + + 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), + } + } } diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 8495bc7982..fe5a0d5ddd 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -30,7 +30,7 @@ use tari_common_types::{ key_branches::TransactionKeyManagerBranch, tari_address::TariAddress, 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::{ @@ -45,7 +45,7 @@ use tari_core::{ proto::base_node::FetchMatchingUtxos, transactions::{ fee::Fee, - key_manager::{TariKeyId, TransactionKeyManagerInterface}, + key_manager::{SecretTransactionKeyManagerInterface, TariKeyId}, tari_amount::MicroMinotari, transaction_components::{ encrypted_data::PaymentId, @@ -80,7 +80,10 @@ use tari_script::{ }; 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::{ @@ -130,7 +133,7 @@ impl where TBackend: OutputManagerBackend + 'static, TWalletConnectivity: WalletConnectivityInterface, - TKeyManagerInterface: TransactionKeyManagerInterface, + TKeyManagerInterface: SecretTransactionKeyManagerInterface, { pub async fn new( config: OutputManagerServiceConfig, @@ -468,9 +471,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();