Skip to content

Commit

Permalink
feat!: export unblinded outputs (#6361)
Browse files Browse the repository at this point in the history
Description
---
Exported unblinded UTXOs. In a previous PR the ability to export
unblinded UTXOs were removed; this PR reinstates that ability. Also
updated the wallet FFI to include all related methods.

Motivation and Context
---
See above.

How Has This Been Tested?
---
System-level testing
Updated the wallet FFI unit test (`test_import_external_utxo`)

What process can a PR reviewer use to test or verify this change?
---
Export utxos

<!-- Checklist -->
<!-- 1. Is the title of your PR in the form that would make nice release
notes? The title, excluding the conventional commit
tag, will be included exactly as is in the CHANGELOG, so please think
about it carefully. -->


Breaking Changes
---

- [ ] None
- [ ] Requires data directory on base node to be deleted
- [ ] Requires hard fork
- [X] Other - Please specify

<!-- Does this include a breaking change? If so, include this line as a
footer -->
BREAKING CHANGE: Wallet FFI interface change to import UTXOs.
  • Loading branch information
hansieodendaal authored Jun 19, 2024
1 parent 4ace36c commit c444b4c
Show file tree
Hide file tree
Showing 27 changed files with 841 additions and 247 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions applications/minotari_app_grpc/proto/transaction.proto
Original file line number Diff line number Diff line change
Expand Up @@ -178,5 +178,7 @@ message UnblindedOutput {
bytes encrypted_data = 12;
// The minimum value of the commitment that is proven by the range proof (in MicroMinotari)
uint64 minimum_value_promise = 13;
// The range proof
RangeProof range_proof = 14;
}

Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@
use std::convert::{TryFrom, TryInto};

use borsh::{BorshDeserialize, BorshSerialize};
use tari_common_types::types::{BulletRangeProof, Commitment, PublicKey};
use tari_common_types::types::{BulletRangeProof, Commitment, PublicKey, RangeProof};
use tari_core::transactions::{
tari_amount::MicroMinotari,
transaction_components::{EncryptedData, TransactionOutput, TransactionOutputVersion},
};
use tari_script::TariScript;
use tari_utilities::ByteArray;

use crate::tari_rpc as grpc;
use crate::{tari_rpc as grpc, tari_rpc::RangeProof as GrpcRangeProof};

impl TryFrom<grpc::TransactionOutput> for TransactionOutput {
type Error = String;
Expand Down Expand Up @@ -113,3 +113,11 @@ impl TryFrom<TransactionOutput> for grpc::TransactionOutput {
})
}
}

impl From<RangeProof> for GrpcRangeProof {
fn from(proof: RangeProof) -> Self {
Self {
proof_bytes: proof.to_vec(),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
use std::convert::{TryFrom, TryInto};

use borsh::{BorshDeserialize, BorshSerialize};
use tari_common_types::types::{PrivateKey, PublicKey};
use tari_common_types::types::{PrivateKey, PublicKey, RangeProof};
use tari_core::transactions::{
tari_amount::MicroMinotari,
transaction_components::{EncryptedData, TransactionOutputVersion, UnblindedOutput},
Expand Down Expand Up @@ -59,6 +59,14 @@ impl TryFrom<UnblindedOutput> for grpc::UnblindedOutput {
covenant,
encrypted_data: output.encrypted_data.to_byte_vec(),
minimum_value_promise: output.minimum_value_promise.into(),
range_proof: {
let proof_bytes = if let Some(proof) = output.range_proof {
proof.to_vec()
} else {
vec![]
};
Some(grpc::RangeProof { proof_bytes })
},
})
}
}
Expand Down Expand Up @@ -99,6 +107,18 @@ impl TryFrom<grpc::UnblindedOutput> for UnblindedOutput {

let minimum_value_promise = MicroMinotari::from(output.minimum_value_promise);

let range_proof = if let Some(proof) = output.range_proof {
if proof.proof_bytes.is_empty() {
None
} else {
let proof =
RangeProof::from_vec(&proof.proof_bytes).map_err(|e| format!("Range proof is invalid: {}", e))?;
Some(proof)
}
} else {
return Err("Range proof not provided".to_string());
};

// zeroize output sensitive data
output.spending_key.zeroize();
output.script_private_key.zeroize();
Expand All @@ -117,6 +137,7 @@ impl TryFrom<grpc::UnblindedOutput> for UnblindedOutput {
covenant,
encrypted_data,
minimum_value_promise,
range_proof,
))
}
}
107 changes: 84 additions & 23 deletions applications/minotari_console_wallet/src/automation/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ use tari_comms::{
use tari_comms_dht::{envelope::NodeDestination, DhtDiscoveryRequester};
use tari_core::transactions::{
tari_amount::{uT, MicroMinotari, Minotari},
transaction_components::{encrypted_data::PaymentId, OutputFeatures, TransactionOutput, WalletOutput},
transaction_components::{
encrypted_data::PaymentId,
OutputFeatures,
TransactionOutput,
UnblindedOutput,
WalletOutput,
},
};
use tari_crypto::ristretto::RistrettoSecretKey;
use tari_utilities::{hex::Hex, ByteArray};
Expand Down Expand Up @@ -811,17 +817,37 @@ pub async fn command_runner(
},
ExportUtxos(args) => match output_service.get_unspent_outputs().await {
Ok(utxos) => {
let utxos: Vec<(WalletOutput, Commitment)> =
utxos.into_iter().map(|v| (v.wallet_output, v.commitment)).collect();
let count = utxos.len();
let sum: MicroMinotari = utxos.iter().map(|utxo| utxo.0.value).sum();
let mut unblinded_utxos: Vec<(UnblindedOutput, Commitment)> = Vec::with_capacity(utxos.len());
for output in utxos {
let unblinded =
UnblindedOutput::from_wallet_output(output.wallet_output, &wallet.key_manager_service)
.await?;
unblinded_utxos.push((unblinded, output.commitment));
}
let count = unblinded_utxos.len();
let sum: MicroMinotari = unblinded_utxos.iter().map(|utxo| utxo.0.value).sum();
if let Some(file) = args.output_file {
if let Err(e) = write_utxos_to_csv_file(utxos, file) {
if let Err(e) = write_utxos_to_csv_file(unblinded_utxos, file, args.with_private_keys) {
eprintln!("ExportUtxos error! {}", e);
}
} else {
for (i, utxo) in utxos.iter().enumerate() {
println!("{}. Value: {} {}", i + 1, utxo.0.value, utxo.0.features);
for (i, utxo) in unblinded_utxos.iter().enumerate() {
println!(
"{}. Value: {}, Spending Key: {:?}, Script Key: {:?}, Features: {}",
i + 1,
utxo.0.value,
if args.with_private_keys {
utxo.0.spending_key.to_hex()
} else {
"*hidden*".to_string()
},
if args.with_private_keys {
utxo.0.script_private_key.to_hex()
} else {
"*hidden*".to_string()
},
utxo.0.features
);
}
}
println!("Total number of UTXOs: {}", count);
Expand Down Expand Up @@ -859,17 +885,37 @@ pub async fn command_runner(
},
ExportSpentUtxos(args) => match output_service.get_spent_outputs().await {
Ok(utxos) => {
let utxos: Vec<(WalletOutput, Commitment)> =
utxos.into_iter().map(|v| (v.wallet_output, v.commitment)).collect();
let count = utxos.len();
let sum: MicroMinotari = utxos.iter().map(|utxo| utxo.0.value).sum();
let mut unblinded_utxos: Vec<(UnblindedOutput, Commitment)> = Vec::with_capacity(utxos.len());
for output in utxos {
let unblinded =
UnblindedOutput::from_wallet_output(output.wallet_output, &wallet.key_manager_service)
.await?;
unblinded_utxos.push((unblinded, output.commitment));
}
let count = unblinded_utxos.len();
let sum: MicroMinotari = unblinded_utxos.iter().map(|utxo| utxo.0.value).sum();
if let Some(file) = args.output_file {
if let Err(e) = write_utxos_to_csv_file(utxos, file) {
if let Err(e) = write_utxos_to_csv_file(unblinded_utxos, file, args.with_private_keys) {
eprintln!("ExportSpentUtxos error! {}", e);
}
} else {
for (i, utxo) in utxos.iter().enumerate() {
println!("{}. Value: {} {}", i + 1, utxo.0.value, utxo.0.features);
for (i, utxo) in unblinded_utxos.iter().enumerate() {
println!(
"{}. Value: {}, Spending Key: {:?}, Script Key: {:?}, Features: {}",
i + 1,
utxo.0.value,
if args.with_private_keys {
utxo.0.spending_key.to_hex()
} else {
"*hidden*".to_string()
},
if args.with_private_keys {
utxo.0.script_private_key.to_hex()
} else {
"*hidden*".to_string()
},
utxo.0.features
);
}
}
println!("Total number of UTXOs: {}", count);
Expand Down Expand Up @@ -1098,22 +1144,26 @@ pub async fn command_runner(
Ok(())
}

fn write_utxos_to_csv_file(utxos: Vec<(WalletOutput, Commitment)>, file_path: PathBuf) -> Result<(), CommandError> {
fn write_utxos_to_csv_file(
utxos: Vec<(UnblindedOutput, Commitment)>,
file_path: PathBuf,
with_private_keys: bool,
) -> Result<(), CommandError> {
let file = File::create(file_path).map_err(|e| CommandError::CSVFile(e.to_string()))?;
let mut csv_file = LineWriter::new(file);
writeln!(
csv_file,
r##""index","version","value","spending_key","commitment","flags","maturity","coinbase_extra","script","covenant","input_data","script_private_key","sender_offset_public_key","ephemeral_commitment","ephemeral_nonce","signature_u_x","signature_u_a","signature_u_y","script_lock_height","encrypted_data","minimum_value_promise""##
r##""index","version","value","spending_key","commitment","output_type","maturity","coinbase_extra","script","covenant","input_data","script_private_key","sender_offset_public_key","ephemeral_commitment","ephemeral_nonce","signature_u_x","signature_u_a","signature_u_y","script_lock_height","encrypted_data","minimum_value_promise","range_proof""##
)
.map_err(|e| CommandError::CSVFile(e.to_string()))?;
.map_err(|e| CommandError::CSVFile(e.to_string()))?;
for (i, (utxo, commitment)) in utxos.iter().enumerate() {
writeln!(
csv_file,
r##""{}","V{}","{}","{}","{}","{:?}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}""##,
r##""{}","V{}","{}","{}","{}","{:?}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}""##,
i + 1,
utxo.version.as_u8(),
utxo.value.0,
utxo.spending_key_id,
if with_private_keys {utxo.spending_key.to_hex()} else { "*hidden*".to_string() },
commitment.to_hex(),
utxo.features.output_type,
utxo.features.maturity,
Expand All @@ -1122,7 +1172,7 @@ fn write_utxos_to_csv_file(utxos: Vec<(WalletOutput, Commitment)>, file_path: Pa
utxo.script.to_hex(),
utxo.covenant.to_bytes().to_hex(),
utxo.input_data.to_hex(),
utxo.script_key_id,
if with_private_keys {utxo.script_private_key.to_hex()} else { "*hidden*".to_string() },
utxo.sender_offset_public_key.to_hex(),
utxo.metadata_signature.ephemeral_commitment().to_hex(),
utxo.metadata_signature.ephemeral_pubkey().to_hex(),
Expand All @@ -1131,9 +1181,20 @@ fn write_utxos_to_csv_file(utxos: Vec<(WalletOutput, Commitment)>, file_path: Pa
utxo.metadata_signature.u_y().to_hex(),
utxo.script_lock_height,
utxo.encrypted_data.to_byte_vec().to_hex(),
utxo.minimum_value_promise.as_u64()
utxo.minimum_value_promise.as_u64(),
if let Some(proof) = utxo.range_proof.clone() {
proof.to_hex()
} else {
"".to_string()
},
)
.map_err(|e| CommandError::CSVFile(e.to_string()))?;
.map_err(|e| CommandError::CSVFile(e.to_string()))?;
debug!(
target: LOG_TARGET,
"UTXO {} exported: {:?}",
i + 1,
utxo
);
}
Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions applications/minotari_console_wallet/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ pub struct WhoisArgs {
pub struct ExportUtxosArgs {
#[clap(short, long)]
pub output_file: Option<PathBuf>,
pub with_private_keys: bool,
}

#[derive(Debug, Args, Clone)]
Expand Down
7 changes: 6 additions & 1 deletion base_layer/core/src/transactions/test_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,12 @@ pub async fn create_wallet_output_with_data<
UtxoTestParams {
value,
script,
features: output_features,
features: output_features.clone(),
minimum_value_promise: if output_features.range_proof_type == RangeProofType::BulletProofPlus {
MicroMinotari::zero()
} else {
value
},
..Default::default()
},
key_manager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ use std::{
};

use serde::{Deserialize, Serialize};
use tari_common_types::types::{ComAndPubSignature, PrivateKey, PublicKey};
use tari_common_types::types::{ComAndPubSignature, PrivateKey, PublicKey, RangeProof};
use tari_script::{ExecutionStack, TariScript};

use super::TransactionOutputVersion;
use super::{RangeProofType, TransactionOutputVersion};
use crate::{
covenants::Covenant,
transactions::{
Expand Down Expand Up @@ -66,6 +66,7 @@ pub struct UnblindedOutput {
pub script_lock_height: u64,
pub encrypted_data: EncryptedData,
pub minimum_value_promise: MicroMinotari,
pub range_proof: Option<RangeProof>,
}

impl UnblindedOutput {
Expand All @@ -86,7 +87,13 @@ impl UnblindedOutput {
covenant: Covenant,
encrypted_data: EncryptedData,
minimum_value_promise: MicroMinotari,
range_proof: Option<RangeProof>,
) -> Self {
let range_proof = if features.range_proof_type == RangeProofType::BulletProofPlus {
range_proof
} else {
None
};
Self {
version,
value,
Expand All @@ -101,9 +108,11 @@ impl UnblindedOutput {
covenant,
encrypted_data,
minimum_value_promise,
range_proof,
}
}

#[allow(clippy::too_many_arguments)]
pub fn new_current_version(
value: MicroMinotari,
spending_key: PrivateKey,
Expand All @@ -117,6 +126,7 @@ impl UnblindedOutput {
covenant: Covenant,
encrypted_data: EncryptedData,
minimum_value_promise: MicroMinotari,
range_proof: Option<RangeProof>,
) -> Self {
Self::new(
TransactionOutputVersion::get_current_version(),
Expand All @@ -132,6 +142,7 @@ impl UnblindedOutput {
covenant,
encrypted_data,
minimum_value_promise,
range_proof,
)
}

Expand All @@ -142,7 +153,7 @@ impl UnblindedOutput {
) -> Result<WalletOutput, TransactionError> {
let spending_key_id = key_manager.import_key(self.spending_key).await?;
let script_key_id = key_manager.import_key(self.script_private_key).await?;
let wallet_output = WalletOutput::new(
let wallet_output = WalletOutput::new_with_rangeproof(
self.version,
self.value,
spending_key_id,
Expand All @@ -156,10 +167,9 @@ impl UnblindedOutput {
self.covenant,
self.encrypted_data,
self.minimum_value_promise,
self.range_proof,
payment_id,
key_manager,
)
.await?;
);
Ok(wallet_output)
}

Expand All @@ -169,6 +179,11 @@ impl UnblindedOutput {
) -> Result<Self, TransactionError> {
let spending_key = key_manager.get_private_key(&output.spending_key_id).await?;
let script_private_key = key_manager.get_private_key(&output.script_key_id).await?;
let range_proof = if output.features.range_proof_type == RangeProofType::BulletProofPlus {
output.range_proof
} else {
None
};
let unblinded_output = UnblindedOutput {
version: output.version,
value: output.value,
Expand All @@ -183,6 +198,7 @@ impl UnblindedOutput {
script_lock_height: output.script_lock_height,
encrypted_data: output.encrypted_data,
minimum_value_promise: output.minimum_value_promise,
range_proof,
};
Ok(unblinded_output)
}
Expand Down
Loading

0 comments on commit c444b4c

Please sign in to comment.