diff --git a/Cargo.lock b/Cargo.lock index 38a526b1d6..d03a44bc5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1653,6 +1653,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "1.0.2" @@ -1673,6 +1682,18 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -3286,6 +3307,7 @@ dependencies = [ "console-subscriber", "crossterm 0.25.0", "digest 0.10.7", + "dirs", "futures 0.3.29", "ledger-transport-hid", "log", @@ -3985,6 +4007,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-float" version = "2.10.1" diff --git a/applications/minotari_console_wallet/Cargo.toml b/applications/minotari_console_wallet/Cargo.toml index 345cd1b111..c4dfb24e96 100644 --- a/applications/minotari_console_wallet/Cargo.toml +++ b/applications/minotari_console_wallet/Cargo.toml @@ -38,6 +38,7 @@ clap = { version = "3.2", features = ["derive", "env"] } config = "0.14.0" crossterm = { version = "0.25.0" } digest = "0.10" +dirs = "5.0" futures = { version = "^0.3.16", default-features = false, features = [ "alloc", ] } diff --git a/applications/minotari_console_wallet/src/automation/commands.rs b/applications/minotari_console_wallet/src/automation/commands.rs index 242de2c0df..82c7cfdb72 100644 --- a/applications/minotari_console_wallet/src/automation/commands.rs +++ b/applications/minotari_console_wallet/src/automation/commands.rs @@ -33,7 +33,7 @@ use std::{ use blake2::Blake2b; use chrono::{DateTime, Utc}; -use digest::{consts::U32, Digest}; +use digest::{consts::U32, crypto_common::rand_core::OsRng, Digest}; use futures::FutureExt; use log::*; use minotari_app_grpc::tls::certs::{generate_self_signed_certs, print_warning, write_cert_to_disk}; @@ -55,7 +55,7 @@ use tari_common_types::{ emoji::EmojiId, tari_address::TariAddress, transaction::TxId, - types::{Commitment, FixedHash, PrivateKey, PublicKey, Signature}, + types::{Commitment, FixedHash, HashOutput, PrivateKey, PublicKey, Signature}, }; use tari_comms::{ connectivity::{ConnectivityEvent, ConnectivityRequester}, @@ -72,7 +72,6 @@ use tari_core::{ tari_amount::{uT, MicroMinotari, Minotari}, transaction_components::{ encrypted_data::PaymentId, - EncryptedData, OutputFeatures, Transaction, TransactionInput, @@ -84,10 +83,13 @@ use tari_core::{ }, }, }; -use tari_crypto::ristretto::{pedersen::PedersenCommitment, RistrettoSecretKey}; +use tari_crypto::{ + keys::SecretKey, + ristretto::{pedersen::PedersenCommitment, RistrettoSecretKey}, +}; use tari_key_manager::key_manager_service::KeyManagerInterface; -use tari_script::{script, CheckSigSchnorrSignature, ExecutionStack, TariScript}; -use tari_utilities::{hex::Hex, ByteArray}; +use tari_script::{script, CheckSigSchnorrSignature}; +use tari_utilities::{encoding::Base58, hex::Hex, ByteArray}; use tokio::{ sync::{broadcast, mpsc}, time::{sleep, timeout}, @@ -95,11 +97,36 @@ use tokio::{ use super::error::CommandError; use crate::{ + automation::{ + utils::{ + get_file_name, + move_session_file_to_session_dir, + out_dir, + read_and_verify, + read_session_info, + write_json_object_to_file_as_line, + write_to_json_file, + }, + Step1SessionInfo, + Step2OutputsForLeader, + Step2OutputsForSelf, + Step3OutputsForParties, + Step3OutputsForSelf, + Step4OutputsForLeader, + }, cli::{CliCommands, MakeItRainTransactionType}, utils::db::{CUSTOM_BASE_NODE_ADDRESS_KEY, CUSTOM_BASE_NODE_PUBLIC_KEY_KEY}, }; pub const LOG_TARGET: &str = "wallet::automation::commands"; +// Faucet file names +pub(crate) const FILE_EXTENSION: &str = "json"; +pub(crate) const SESSION_INFO: &str = "step_1_session_info"; +pub(crate) const STEP_2_LEADER: &str = "step_2_for_leader_from_"; +pub(crate) const STEP_2_SELF: &str = "step_2_for_self"; +pub(crate) const STEP_3_SELF: &str = "step_3_for_self"; +pub(crate) const STEP_3_PARTIES: &str = "step_3_for_parties"; +pub(crate) const STEP_4_LEADER: &str = "step_4_for_leader_from_"; #[derive(Debug)] pub struct SentTransaction {} @@ -149,7 +176,7 @@ pub async fn burn_tari( async fn encumber_aggregate_utxo( mut wallet_transaction_service: TransactionServiceHandle, fee_per_gram: MicroMinotari, - output_hash: String, + output_hash: HashOutput, expected_commitment: PedersenCommitment, script_input_shares: HashMap, script_signature_public_nonces: Vec, @@ -717,19 +744,81 @@ pub async fn command_runner( Err(e) => eprintln!("BurnMinotari error! {}", e), } }, + FaucetGenerateSessionInfo(args) => { + let commitment = if let Ok(val) = Commitment::from_hex(&args.commitment) { + val + } else { + eprintln!("\nError: Invalid 'commitment' provided!\n"); + continue; + }; + let hash = if let Ok(val) = FixedHash::from_hex(&args.output_hash) { + val + } else { + eprintln!("\nError: Invalid 'output_hash' provided!\n"); + continue; + }; + + if args.verify_unspent_outputs { + let unspent_outputs = transaction_service.fetch_unspent_outputs(vec![hash]).await?; + if unspent_outputs.is_empty() { + eprintln!( + "\nError: Output with output_hash '{}' has already been spent!\n", + args.output_hash + ); + continue; + } + if unspent_outputs[0].commitment() != &commitment { + eprintln!( + "\nError: Mismatched commitment '{}' and output_hash '{}'; not for the same output!\n", + args.commitment, args.output_hash + ); + continue; + } + } + + let mut session_id = PrivateKey::random(&mut OsRng).to_base58(); + session_id.truncate(16); + let session_info = Step1SessionInfo { + session_id: session_id.clone(), + commitment_to_spend: args.commitment, + output_hash: args.output_hash, + recipient_address: args.recipient_address, + fee_per_gram: args.fee_per_gram, + }; + let out_dir = out_dir(&session_info.session_id)?; + let out_file = out_dir.join(get_file_name(SESSION_INFO, None)); + write_to_json_file(&out_file, true, session_info)?; + println!(); + println!("Concluded step 1 'faucet-generate-session-info'"); + println!("Your session ID is: '{}'", session_id); + println!("Your session's output directory is: '{}'", out_dir.display()); + println!("Session info saved to: '{}'", out_file.display()); + println!("Send '{}' to parties for step 2", get_file_name(SESSION_INFO, None)); + println!(); + }, FaucetCreatePartyDetails(args) => { + if args.alias.is_empty() || args.alias.contains(" ") { + eprintln!("\nError: Alias cannot contain spaces!\n"); + continue; + } + if args.alias.chars().any(|c| !c.is_alphanumeric() && c != '_') { + eprintln!("\nError: Alias contains invalid characters! Only alphanumeric and '_' are allowed.\n"); + continue; + } + let wallet_spend_key_id = wallet.get_wallet_id().await?.wallet_node_key_id.clone(); let wallet_public_spend_key = key_manager_service .get_public_key_at_key_id(&wallet_spend_key_id) .await?; - let (script_nonce_key_id, public_script_nonce) = key_manager_service.get_random_key().await?; - + let (script_nonce_key_id, public_script_nonce_key) = key_manager_service.get_random_key().await?; let (sender_offset_key_id, public_sender_offset_key) = key_manager_service.get_random_key().await?; - - let (sender_offset_nonce_key_id, public_sender_offset_nonce) = + let (sender_offset_nonce_key_id, public_sender_offset_nonce_key) = key_manager_service.get_random_key().await?; - let commitment = Commitment::from_hex(&args.commitment)?; + // Read session info + let session_info = read_session_info(&args.session_id, Some(args.input_file.clone()))?; + + let commitment = Commitment::from_hex(&session_info.commitment_to_spend)?; let commitment_hash: [u8; 32] = DomainSeparatedConsensusHasher::>::new("com_hash") .chain(&commitment) @@ -738,7 +827,8 @@ pub async fn command_runner( let shared_secret = key_manager_service .get_diffie_hellman_shared_secret( &sender_offset_key_id, - args.recipient_address + session_info + .recipient_address .public_view_key() .ok_or(CommandError::InvalidArgument("Missing public view key".to_string()))?, ) @@ -749,65 +839,73 @@ pub async fn command_runner( .sign_script_message(&wallet_spend_key_id, &commitment_hash) .await?; - println!( - "Party details created with: - 1. script input share: ({},{},{}), - 3. wallet public spend key_id: {}, - 4. spend nonce key_id: {}, - 5. public spend nonce key: {}, - 6. sender offset key_id: {}, - 7. public sender offset key: {}, - 8. sender offset nonce key_id: {}, - 9. public sender offset nonce key: {}, - 10. public shared secret: {}", + let out_dir = out_dir(&args.session_id)?; + let step_2_outputs_for_leader = Step2OutputsForLeader { + script_input_signature, wallet_public_spend_key, - script_input_signature.get_signature().to_hex(), - script_input_signature.get_public_nonce().to_hex(), + public_script_nonce_key, + public_sender_offset_key, + public_sender_offset_nonce_key, + dh_shared_secret_public_key: shared_secret_public_key, + }; + let out_file_leader = out_dir.join(get_file_name(STEP_2_LEADER, Some(args.alias.clone()))); + write_json_object_to_file_as_line(&out_file_leader, true, session_info.clone())?; + write_json_object_to_file_as_line(&out_file_leader, false, step_2_outputs_for_leader)?; + + let step_2_outputs_for_self = Step2OutputsForSelf { + alias: args.alias.clone(), wallet_spend_key_id, script_nonce_key_id, - public_script_nonce, sender_offset_key_id, - public_sender_offset_key, sender_offset_nonce_key_id, - public_sender_offset_nonce, - shared_secret_public_key + }; + let out_file_self = out_dir.join(get_file_name(STEP_2_SELF, None)); + write_json_object_to_file_as_line(&out_file_self, true, session_info)?; + write_json_object_to_file_as_line(&out_file_self, false, step_2_outputs_for_self)?; + + println!(); + println!("Concluded step 2 'faucet-create-party-details'"); + println!("Your session's output directory is '{}'", out_dir.display()); + move_session_file_to_session_dir(&args.session_id, &args.input_file)?; + println!( + "Send '{}' to leader for step 3", + get_file_name(STEP_2_LEADER, Some(args.alias)) ); + println!(); }, FaucetEncumberAggregateUtxo(args) => { + // Read session info + let session_info = read_session_info(&args.session_id, None)?; + #[allow(clippy::mutable_key_type)] let mut input_shares = HashMap::new(); - for share in args.script_input_shares { - let data = share.split(',').collect::>(); - let public_key = PublicKey::from_hex(data[0])?; - let signature = PrivateKey::from_hex(data[1])?; - let public_nonce = PublicKey::from_hex(data[2])?; - let sig = CheckSigSchnorrSignature::new(public_nonce, signature); - input_shares.insert(public_key, sig); + let mut script_signature_public_nonces = Vec::with_capacity(args.input_file_names.len()); + let mut sender_offset_public_key_shares = Vec::with_capacity(args.input_file_names.len()); + let mut metadata_ephemeral_public_key_shares = Vec::with_capacity(args.input_file_names.len()); + let mut dh_shared_secret_shares = Vec::with_capacity(args.input_file_names.len()); + for file_name in args.input_file_names { + // Read party input + let party_info = + read_and_verify::(&args.session_id, &file_name, &session_info)?; + input_shares.insert(party_info.wallet_public_spend_key, party_info.script_input_signature); + script_signature_public_nonces.push(party_info.public_script_nonce_key); + sender_offset_public_key_shares.push(party_info.public_sender_offset_key); + metadata_ephemeral_public_key_shares.push(party_info.public_sender_offset_nonce_key); + dh_shared_secret_shares.push(party_info.dh_shared_secret_public_key); } match encumber_aggregate_utxo( transaction_service.clone(), - args.fee_per_gram, - args.output_hash, - Commitment::from_hex(&args.commitment)?, + session_info.fee_per_gram, + FixedHash::from_hex(&session_info.output_hash) + .map_err(|e| CommandError::InvalidArgument(e.to_string()))?, + Commitment::from_hex(&session_info.commitment_to_spend)?, input_shares, - args.script_signature_public_nonces - .iter() - .map(|v| v.clone().into()) - .collect::>(), - args.sender_offset_public_key_shares - .iter() - .map(|v| v.clone().into()) - .collect::>(), - args.metadata_ephemeral_public_key_shares - .iter() - .map(|v| v.clone().into()) - .collect::>(), - args.dh_shared_secret_shares - .iter() - .map(|v| v.clone().into()) - .collect::>(), - args.recipient_address, + script_signature_public_nonces, + sender_offset_public_key_shares, + metadata_ephemeral_public_key_shares, + dh_shared_secret_shares, + session_info.recipient_address.clone(), ) .await { @@ -818,178 +916,184 @@ pub async fn command_runner( total_metadata_ephemeral_public_key, total_script_nonce, )) => { - println!( - "Encumbered aggregate UTXO: - 1. tx_id: {}, - 2. input_commitment: {}, - 3. input_stack: {}, - 4. input_script: {}, - 5. total_script_key: {}, - 6. script_signature_ephemeral_commitment: {}, - 7. script_signature_ephemeral_pubkey: {}, - 8. output_commitment: {}, - 9. output_hash: {}, - 10. sender_offset_pubkey: {}, - 11. meta_signature_ephemeral_commitment: {}, - 12. meta_signature_ephemeral_pubkey: {}, - 13. total_public_offset: {}, - 14. encrypted_data: {}, - 15. output_features: {}", - tx_id, - transaction.body.inputs()[0].commitment().unwrap().to_hex(), - transaction.body.inputs()[0].input_data.to_hex(), - transaction.body.inputs()[0].script().unwrap().to_hex(), - script_pubkey.to_hex(), - transaction.body.inputs()[0] + let out_dir = out_dir(&args.session_id)?; + let step_3_outputs_for_self = Step3OutputsForSelf { tx_id }; + let out_file = out_dir.join(get_file_name(STEP_3_SELF, None)); + write_json_object_to_file_as_line(&out_file, true, session_info.clone())?; + write_json_object_to_file_as_line(&out_file, false, step_3_outputs_for_self)?; + + let step_3_outputs_for_parties = Step3OutputsForParties { + input_stack: transaction.body.inputs()[0].clone().input_data, + input_script: transaction.body.inputs()[0].script().unwrap().clone(), + total_script_key: script_pubkey, + script_signature_ephemeral_commitment: transaction.body.inputs()[0] .script_signature .ephemeral_commitment() - .to_hex(), - total_script_nonce.to_hex(), - transaction.body.outputs()[0].commitment().to_hex(), - transaction.body.outputs()[0].hash().to_hex(), - transaction.body.outputs()[0].sender_offset_public_key.to_hex(), - transaction.body.outputs()[0] + .clone(), + script_signature_ephemeral_pubkey: total_script_nonce, + output_commitment: transaction.body.outputs()[0].commitment().clone(), + sender_offset_pubkey: transaction.body.outputs()[0].clone().sender_offset_public_key, + metadata_signature_ephemeral_commitment: transaction.body.outputs()[0] .metadata_signature .ephemeral_commitment() - .to_hex(), - total_metadata_ephemeral_public_key.to_hex(), - transaction.script_offset.to_hex(), - transaction.body.outputs()[0].encrypted_data.to_hex(), - serde_json::to_string(&transaction.body.outputs()[0].features) - .unwrap_or("Could not serialize output features".to_string()) - ) + .clone(), + metadata_signature_ephemeral_pubkey: total_metadata_ephemeral_public_key, + encrypted_data: transaction.body.outputs()[0].clone().encrypted_data, + output_features: transaction.body.outputs()[0].clone().features, + }; + let out_file = out_dir.join(get_file_name(STEP_3_PARTIES, None)); + write_json_object_to_file_as_line(&out_file, true, session_info.clone())?; + write_json_object_to_file_as_line(&out_file, false, step_3_outputs_for_parties)?; }, - Err(e) => println!("Encumber aggregate transaction error! {}", e), - } - }, - FaucetSpendAggregateUtxo(args) => { - let mut offset = PrivateKey::default(); - for key in args.script_offset_keys { - let secret_key = - PrivateKey::from_hex(&key).map_err(|e| CommandError::InvalidArgument(e.to_string()))?; - offset = &offset + &secret_key; + Err(e) => eprintln!("Error: Encumber aggregate transaction error! {}", e), } - match finalise_aggregate_utxo( - transaction_service.clone(), - args.tx_id, - args.meta_signatures - .iter() - .map(|sgn| sgn.clone().into()) - .collect::>(), - args.script_signatures - .iter() - .map(|sgn| sgn.clone().into()) - .collect::>(), - offset, - ) - .await - { - Ok(_v) => println!("Transactions successfully completed"), - Err(e) => println!("Error completing transaction! {}", e), - } + println!(); + println!("Concluded step 3 'faucet-encumber-aggregate-utxo'"); + println!("Send '{}' to parties for step 4", get_file_name(STEP_3_PARTIES, None)); + println!(); }, - FaucetCreateScriptSig(args) => { - let script = TariScript::from_hex(&args.input_script) - .map_err(|e| CommandError::InvalidArgument(e.to_string()))?; - let input_data = ExecutionStack::from_hex(&args.input_stack) - .map_err(|e| CommandError::InvalidArgument(e.to_string()))?; - let commitment = - Commitment::from_hex(&args.commitment).map_err(|e| CommandError::InvalidArgument(e.to_string()))?; - let ephemeral_commitment = Commitment::from_hex(&args.ephemeral_commitment) - .map_err(|e| CommandError::InvalidArgument(e.to_string()))?; - let ephemeral_pubkey = PublicKey::from(args.ephemeral_pubkey); + FaucetCreateInputOutputSigs(args) => { + // Read session info + let session_info = read_session_info(&args.session_id, None)?; + // Read leader input + let leader_info = read_and_verify::( + &args.session_id, + &get_file_name(STEP_3_PARTIES, None), + &session_info, + )?; + // Read own party info + let party_info = read_and_verify::( + &args.session_id, + &get_file_name(STEP_2_SELF, None), + &session_info, + )?; + + // Script signature let challenge = TransactionInput::build_script_signature_challenge( &TransactionInputVersion::get_current_version(), - &ephemeral_commitment, - &ephemeral_pubkey, - &script, - &input_data, - &args.total_script_key.into(), - &commitment, + &leader_info.script_signature_ephemeral_commitment, + &leader_info.script_signature_ephemeral_pubkey, + &leader_info.input_script, + &leader_info.input_stack, + &leader_info.total_script_key, + &Commitment::from_hex(&session_info.commitment_to_spend)?, ); + let mut script_signature = Signature::default(); match key_manager_service - .sign_with_nonce_and_message(&args.private_key_id, &args.secret_nonce_key_id, &challenge) + .sign_with_nonce_and_message( + &party_info.wallet_spend_key_id, + &party_info.script_nonce_key_id, + &challenge, + ) .await { Ok(signature) => { - println!( - "Script signature created: - 1. signature: ({},{})", - signature.get_signature().to_hex(), - signature.get_public_nonce().to_hex(), - ) + script_signature = signature; }, - Err(e) => eprintln!("SignMessage error! {}", e), + Err(e) => eprintln!("Error: Script signature SignMessage error! {}", e), } - }, - FaucetCreateMetaSig(args) => { - let offset = key_manager_service - .get_script_offset(&vec![args.secret_script_key_id], &vec![args - .secret_sender_offset_key_id + + // Metadata signature + let script_offset = key_manager_service + .get_script_offset(&vec![party_info.wallet_spend_key_id], &vec![party_info + .sender_offset_key_id .clone()]) .await?; - let script = script!(PushPubKey(Box::new(args.recipient_address.public_spend_key().clone()))); - let commitment = - Commitment::from_hex(&args.commitment).map_err(|e| CommandError::InvalidArgument(e.to_string()))?; - let covenant = Covenant::default(); - let encrypted_data = EncryptedData::from_hex(&args.encrypted_data) - .map_err(|e| CommandError::InvalidArgument(e.to_string()))?; - let output_features = serde_json::from_str(&args.output_features) - .map_err(|e| CommandError::InvalidArgument(e.to_string()))?; - let ephemeral_commitment = Commitment::from_hex(&args.ephemeral_commitment) - .map_err(|e| CommandError::InvalidArgument(e.to_string()))?; - let ephemeral_pubkey = PublicKey::from_hex(&args.ephemeral_pubkey) - .map_err(|e| CommandError::InvalidArgument(e.to_string()))?; - let minimum_value_promise = MicroMinotari::zero(); - trace!( - target: LOG_TARGET, - "version: {:?}", - TransactionOutputVersion::get_current_version() - ); - trace!(target: LOG_TARGET, "script: {:?}", script); - trace!(target: LOG_TARGET, "output features: {:?}", output_features); - let offsetkey: PublicKey = args.total_meta_key.clone().into(); - trace!(target: LOG_TARGET, "sender_offset_public_key: {:?}", offsetkey); - trace!(target: LOG_TARGET, "ephemeral_commitment: {:?}", ephemeral_commitment); - trace!(target: LOG_TARGET, "ephemeral_pubkey: {:?}", ephemeral_pubkey); - trace!(target: LOG_TARGET, "commitment: {:?}", commitment); - trace!(target: LOG_TARGET, "covenant: {:?}", covenant); - trace!(target: LOG_TARGET, "encrypted_value: {:?}", encrypted_data); - trace!(target: LOG_TARGET, "minimum_value_promise: {:?}", minimum_value_promise); let challenge = TransactionOutput::build_metadata_signature_challenge( &TransactionOutputVersion::get_current_version(), - &script, - &output_features, - &args.total_meta_key.into(), - &ephemeral_commitment, - &ephemeral_pubkey, - &commitment, - &covenant, - &encrypted_data, - minimum_value_promise, + &script!(PushPubKey(Box::new( + session_info.recipient_address.public_spend_key().clone() + ))), + &leader_info.output_features, + &leader_info.sender_offset_pubkey, + &leader_info.metadata_signature_ephemeral_commitment, + &leader_info.metadata_signature_ephemeral_pubkey, + &leader_info.output_commitment, + &Covenant::default(), + &leader_info.encrypted_data, + MicroMinotari::zero(), ); - trace!(target: LOG_TARGET, "meta challenge: {:?}", challenge); + + let mut metadata_signature = Signature::default(); match key_manager_service .sign_with_nonce_and_message( - &args.secret_sender_offset_key_id, - &args.secret_nonce_key_id, + &party_info.sender_offset_key_id, + &party_info.sender_offset_nonce_key_id, &challenge, ) .await { Ok(signature) => { - println!( - "Metadata signature created: - 1. signature: ({},{}), - 2. script offset: {}", - signature.get_signature().to_hex(), - signature.get_public_nonce().to_hex(), - offset.to_hex(), - ) + metadata_signature = signature; + }, + Err(e) => eprintln!("Error: Metadata signature SignMessage error! {}", e), + } + + if script_signature.get_signature() == Signature::default().get_signature() || + metadata_signature.get_signature() == Signature::default().get_signature() + { + eprintln!("Error: Script and/or metadata signatures not created!") + } else { + let step_4_outputs_for_leader = Step4OutputsForLeader { + script_signature, + metadata_signature, + script_offset, + }; + + let out_dir = out_dir(&args.session_id)?; + let out_file = out_dir.join(get_file_name(STEP_4_LEADER, Some(party_info.alias.clone()))); + write_json_object_to_file_as_line(&out_file, true, session_info.clone())?; + write_json_object_to_file_as_line(&out_file, false, step_4_outputs_for_leader)?; + + println!(); + println!("Concluded step 4 'faucet-create-input-output-sigs'"); + println!( + "Send '{}' to leader for step 5", + get_file_name(STEP_4_LEADER, Some(party_info.alias)) + ); + println!(); + } + }, + FaucetSpendAggregateUtxo(args) => { + // Read session info + let session_info = read_session_info(&args.session_id, None)?; + + let mut metadata_signatures = Vec::with_capacity(args.input_file_names.len()); + let mut script_signatures = Vec::with_capacity(args.input_file_names.len()); + let mut offset = PrivateKey::default(); + for file_name in args.input_file_names { + // Read party input + let party_info = + read_and_verify::(&args.session_id, &file_name, &session_info)?; + metadata_signatures.push(party_info.metadata_signature); + script_signatures.push(party_info.script_signature); + offset = &offset + &party_info.script_offset; + } + + // Read own party info + let leader_info = read_and_verify::( + &args.session_id, + &get_file_name(STEP_3_SELF, None), + &session_info, + )?; + + match finalise_aggregate_utxo( + transaction_service.clone(), + leader_info.tx_id.as_u64(), + metadata_signatures, + script_signatures, + offset, + ) + .await + { + Ok(_v) => { + println!(); + println!("Concluded step 5 'faucet-spend-aggregate-utxo'"); + println!(); }, - Err(e) => eprintln!("SignMessage error! {}", e), + Err(e) => println!("Error: Error completing transaction! {}", e), } }, SendMinotari(args) => { diff --git a/applications/minotari_console_wallet/src/automation/mod.rs b/applications/minotari_console_wallet/src/automation/mod.rs index 1470375082..acdc48c0c5 100644 --- a/applications/minotari_console_wallet/src/automation/mod.rs +++ b/applications/minotari_console_wallet/src/automation/mod.rs @@ -22,5 +22,80 @@ pub mod commands; pub mod error; +mod utils; // removed temporarily add back in when used. // mod prompt; + +use serde::{Deserialize, Serialize}; +use tari_common_types::{ + tari_address::TariAddress, + transaction::TxId, + types::{Commitment, PrivateKey, PublicKey, Signature}, +}; +use tari_core::transactions::{ + key_manager::TariKeyId, + tari_amount::MicroMinotari, + transaction_components::{EncryptedData, OutputFeatures}, +}; +use tari_script::{CheckSigSchnorrSignature, ExecutionStack, TariScript}; + +// Outputs for self with `FaucetCreatePartyDetails` +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +struct Step1SessionInfo { + session_id: String, + fee_per_gram: MicroMinotari, + commitment_to_spend: String, + output_hash: String, + recipient_address: TariAddress, +} + +// Outputs for self with `FaucetCreatePartyDetails` +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +struct Step2OutputsForSelf { + alias: String, + wallet_spend_key_id: TariKeyId, + script_nonce_key_id: TariKeyId, + sender_offset_key_id: TariKeyId, + sender_offset_nonce_key_id: TariKeyId, +} + +// Outputs for leader with `FaucetCreatePartyDetails` +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +struct Step2OutputsForLeader { + script_input_signature: CheckSigSchnorrSignature, + wallet_public_spend_key: PublicKey, + public_script_nonce_key: PublicKey, + public_sender_offset_key: PublicKey, + public_sender_offset_nonce_key: PublicKey, + dh_shared_secret_public_key: PublicKey, +} + +// Outputs for self with `FaucetEncumberAggregateUtxo` +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +struct Step3OutputsForSelf { + tx_id: TxId, +} + +// Outputs for parties with `FaucetEncumberAggregateUtxo` +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +struct Step3OutputsForParties { + input_stack: ExecutionStack, + input_script: TariScript, + total_script_key: PublicKey, + script_signature_ephemeral_commitment: Commitment, + script_signature_ephemeral_pubkey: PublicKey, + output_commitment: Commitment, + sender_offset_pubkey: PublicKey, + metadata_signature_ephemeral_commitment: Commitment, + metadata_signature_ephemeral_pubkey: PublicKey, + encrypted_data: EncryptedData, + output_features: OutputFeatures, +} + +// Outputs for leader with `FaucetCreateScriptSig` and `FaucetCreateMetaSig` +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +struct Step4OutputsForLeader { + script_signature: Signature, + metadata_signature: Signature, + script_offset: PrivateKey, +} diff --git a/applications/minotari_console_wallet/src/automation/utils.rs b/applications/minotari_console_wallet/src/automation/utils.rs new file mode 100644 index 0000000000..09c079d3b0 --- /dev/null +++ b/applications/minotari_console_wallet/src/automation/utils.rs @@ -0,0 +1,217 @@ +// Copyright 2020. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{ + fs, + fs::{File, OpenOptions}, + io::{BufRead, BufReader, Write}, + path::{Path, PathBuf}, +}; + +use serde::{de::DeserializeOwned, Serialize}; + +use crate::automation::{ + commands::{FILE_EXTENSION, SESSION_INFO}, + error::CommandError, + Step1SessionInfo, +}; + +#[derive(Debug)] +pub(crate) struct PartialRead { + pub(crate) lines_to_read: usize, + pub(crate) lines_to_skip: usize, +} + +/// Reads an entire file into a single JSON object +pub(crate) fn json_from_file_single_object, T: DeserializeOwned>( + path: P, + partial_read: Option, +) -> Result { + if let Some(val) = partial_read { + let lines = BufReader::new( + File::open(path.as_ref()) + .map_err(|e| CommandError::JsonFile(format!("{e} '{}'", path.as_ref().display())))?, + ) + .lines() + .take(val.lines_to_read) + .skip(val.lines_to_skip); + let mut json_str = String::new(); + for line in lines { + let line = line.map_err(|e| CommandError::JsonFile(format!("{e} '{}'", path.as_ref().display())))?; + json_str.push_str(&line); + } + serde_json::from_str(&json_str) + .map_err(|e| CommandError::JsonFile(format!("{e} '{}'", path.as_ref().display()))) + } else { + serde_json::from_reader(BufReader::new( + File::open(path.as_ref()) + .map_err(|e| CommandError::JsonFile(format!("{e} '{}'", path.as_ref().display())))?, + )) + .map_err(|e| CommandError::JsonFile(format!("{e} '{}'", path.as_ref().display()))) + } +} + +/// Write a single JSON object to file as a single line +pub(crate) fn write_json_object_to_file_as_line( + file: &Path, + reset_file: bool, + outputs: T, +) -> Result<(), CommandError> { + if let Some(file_path) = file.parent() { + if !file_path.exists() { + fs::create_dir_all(file_path).map_err(|e| CommandError::JsonFile(format!("{} ({})", e, file.display())))?; + } + } + if reset_file && file.exists() { + fs::remove_file(file).map_err(|e| CommandError::JsonFile(e.to_string()))?; + } + append_json_line_to_file(file, outputs)?; + Ok(()) +} + +fn append_json_line_to_file, T: Serialize>(file: P, output: T) -> Result<(), CommandError> { + fs::create_dir_all(file.as_ref().parent().unwrap()).map_err(|e| CommandError::JsonFile(e.to_string()))?; + let mut file_object = OpenOptions::new() + .create(true) + .append(true) + .open(file) + .map_err(|e| CommandError::JsonFile(e.to_string()))?; + let json = serde_json::to_string(&output).map_err(|e| CommandError::JsonFile(e.to_string()))?; + writeln!(file_object, "{json}").map_err(|e| CommandError::JsonFile(e.to_string()))?; + Ok(()) +} + +/// Write outputs to a JSON file +pub(crate) fn write_to_json_file(file: &Path, reset_file: bool, data: T) -> Result<(), CommandError> { + if let Some(file_path) = file.parent() { + if !file_path.exists() { + fs::create_dir_all(file_path).map_err(|e| CommandError::JsonFile(format!("{} ({})", e, file.display())))?; + } + } + if reset_file && file.exists() { + fs::remove_file(file).map_err(|e| CommandError::JsonFile(e.to_string()))?; + } + append_to_json_file(file, data)?; + Ok(()) +} + +fn append_to_json_file, T: Serialize>(file: P, data: T) -> Result<(), CommandError> { + fs::create_dir_all(file.as_ref().parent().unwrap()).map_err(|e| CommandError::JsonFile(e.to_string()))?; + let mut file_object = OpenOptions::new() + .create(true) + .append(true) + .open(file) + .map_err(|e| CommandError::JsonFile(e.to_string()))?; + let json = serde_json::to_string_pretty(&data).map_err(|e| CommandError::JsonFile(e.to_string()))?; + writeln!(file_object, "{json}").map_err(|e| CommandError::JsonFile(e.to_string()))?; + Ok(()) +} + +/// Return the output directory for the session +pub(crate) fn out_dir(session_id: &str) -> Result { + let base_dir = dirs::cache_dir().ok_or(CommandError::InvalidArgument( + "Could not find cache directory".to_string(), + ))?; + Ok(base_dir.join("tari_faucets").join(session_id)) +} + +/// Move the session file to the session directory +pub(crate) fn move_session_file_to_session_dir(session_id: &str, input_file: &PathBuf) -> Result<(), CommandError> { + let out_dir = out_dir(session_id)?; + let session_file = out_dir.join(get_file_name(SESSION_INFO, None)); + if input_file != &session_file { + fs::copy(input_file.clone(), session_file.clone())?; + fs::remove_file(input_file.clone())?; + println!( + "Session info file '{}' moved to '{}'", + input_file.display(), + session_file.display() + ); + } + Ok(()) +} + +/// Read the session info from the session directory +pub(crate) fn read_session_info( + session_id: &str, + session_file: Option, +) -> Result { + let file_path = if let Some(file) = session_file { + file + } else { + out_dir(session_id)?.join(get_file_name(SESSION_INFO, None)) + }; + let session_info = json_from_file_single_object::<_, Step1SessionInfo>(&file_path, None)?; + if session_info.session_id != session_id { + return Err(CommandError::InvalidArgument(format!( + "Session ID in session info file '{}' mismatch", + get_file_name(SESSION_INFO, None) + ))); + } + Ok(session_info) +} + +/// Read the inputs from the session directory and verify the header +pub(crate) fn read_and_verify( + session_id: &str, + file_name: &str, + session_info: &Step1SessionInfo, +) -> Result { + let out_dir = out_dir(session_id)?; + let header = json_from_file_single_object::<_, Step1SessionInfo>( + &out_dir.join(file_name), + Some(PartialRead { + lines_to_read: 1, + lines_to_skip: 0, + }), + )?; + if session_id != header.session_id { + return Err(CommandError::InvalidArgument(format!( + "Session ID in header for file '{}' mismatch", + file_name + ))); + } + if session_info != &header { + return Err(CommandError::InvalidArgument(format!( + "Session info in header for file '{}' mismatch", + file_name + ))); + } + json_from_file_single_object::<_, T>( + &out_dir.join(file_name), + Some(PartialRead { + lines_to_read: usize::MAX, + lines_to_skip: 1, + }), + ) +} + +/// Create the file name with the given stem and optional suffix +pub(crate) fn get_file_name(stem: &str, suffix: Option) -> String { + let mut file_name = stem.to_string(); + if let Some(suffix) = suffix { + file_name.push_str(&suffix); + } + file_name.push('.'); + file_name.push_str(FILE_EXTENSION); + file_name +} diff --git a/applications/minotari_console_wallet/src/cli.rs b/applications/minotari_console_wallet/src/cli.rs index 3f10360494..1a5b7d3868 100644 --- a/applications/minotari_console_wallet/src/cli.rs +++ b/applications/minotari_console_wallet/src/cli.rs @@ -28,14 +28,11 @@ use std::{ use chrono::{DateTime, Utc}; use clap::{Args, Parser, Subcommand}; -use minotari_app_utilities::{ - common_cli_args::CommonCliArgs, - utilities::{UniPublicKey, UniSignature}, -}; +use minotari_app_utilities::{common_cli_args::CommonCliArgs, utilities::UniPublicKey}; use tari_common::configuration::{ConfigOverrideProvider, Network}; use tari_common_types::tari_address::TariAddress; use tari_comms::multiaddr::Multiaddr; -use tari_core::transactions::{key_manager::TariKeyId, tari_amount, tari_amount::MicroMinotari}; +use tari_core::transactions::{tari_amount, tari_amount::MicroMinotari}; use tari_key_manager::SeedWords; use tari_utilities::{ hex::{Hex, HexError}, @@ -119,11 +116,11 @@ pub enum CliCommands { GetBalance, SendMinotari(SendMinotariArgs), BurnMinotari(BurnMinotariArgs), + FaucetGenerateSessionInfo(FaucetGenerateSessionInfoArgs), + FaucetCreatePartyDetails(FaucetCreatePartyDetailsArgs), FaucetEncumberAggregateUtxo(FaucetEncumberAggregateUtxoArgs), + FaucetCreateInputOutputSigs(FaucetCreateInputOutputSigArgs), FaucetSpendAggregateUtxo(FaucetSpendAggregateUtxoArgs), - FaucetCreatePartyDetails(FaucetCreatePartyDetailsArgs), - FaucetCreateScriptSig(FaucetCreateScriptSigArgs), - FaucetCreateMetaSig(FaucetCreateMetaSigArgs), SendOneSidedToStealthAddress(SendMinotariArgs), MakeItRain(MakeItRainArgs), CoinSplit(CoinSplitArgs), @@ -166,123 +163,49 @@ pub struct BurnMinotariArgs { } #[derive(Debug, Args, Clone)] -pub struct FaucetCreateKeyPairArgs { - #[clap(long)] - pub key_branch: String, -} - -#[derive(Debug, Args, Clone)] -pub struct FaucetCreateAggregateSignatureUtxoArgs { - #[clap(long)] - pub amount: MicroMinotari, +pub struct FaucetGenerateSessionInfoArgs { #[clap(long)] pub fee_per_gram: MicroMinotari, #[clap(long)] - pub n: u8, - #[clap(long)] - pub m: u8, + pub commitment: String, #[clap(long)] - pub message: String, + pub output_hash: String, #[clap(long)] - pub maturity: u64, + pub recipient_address: TariAddress, #[clap(long)] - pub public_keys: Vec, + pub verify_unspent_outputs: bool, } #[derive(Debug, Args, Clone)] pub struct FaucetCreatePartyDetailsArgs { #[clap(long)] - pub commitment: String, - #[clap(long)] - pub recipient_address: TariAddress, -} - -#[derive(Debug, Args, Clone)] -pub struct FaucetSignMessageArgs { + pub session_id: String, #[clap(long)] - pub private_key_id: TariKeyId, + pub input_file: PathBuf, #[clap(long)] - pub challenge: String, + pub alias: String, } #[derive(Debug, Args, Clone)] pub struct FaucetEncumberAggregateUtxoArgs { #[clap(long)] - pub fee_per_gram: MicroMinotari, - #[clap(long)] - pub commitment: String, - #[clap(long)] - pub output_hash: String, - #[clap(long)] - pub script_input_shares: Vec, - #[clap(long)] - pub script_public_key_shares: Vec, - #[clap(long)] - pub script_signature_public_nonces: Vec, - #[clap(long)] - pub sender_offset_public_key_shares: Vec, - #[clap(long)] - pub metadata_ephemeral_public_key_shares: Vec, - #[clap(long)] - pub dh_shared_secret_shares: Vec, + pub session_id: String, #[clap(long)] - pub recipient_address: TariAddress, -} - -#[derive(Debug, Args, Clone)] -pub struct FaucetSpendAggregateUtxoArgs { - #[clap(long)] - pub tx_id: u64, - #[clap(long)] - pub meta_signatures: Vec, - #[clap(long)] - pub script_signatures: Vec, - #[clap(long)] - pub script_offset_keys: Vec, + pub input_file_names: Vec, } #[derive(Debug, Args, Clone)] -pub struct FaucetCreateScriptSigArgs { - #[clap(long)] - pub private_key_id: TariKeyId, - #[clap(long)] - pub secret_nonce_key_id: TariKeyId, - #[clap(long)] - pub input_script: String, - #[clap(long)] - pub input_stack: String, - #[clap(long)] - pub ephemeral_commitment: String, - #[clap(long)] - pub ephemeral_pubkey: UniPublicKey, +pub struct FaucetCreateInputOutputSigArgs { #[clap(long)] - pub total_script_key: UniPublicKey, - #[clap(long)] - pub commitment: String, + pub session_id: String, } #[derive(Debug, Args, Clone)] -pub struct FaucetCreateMetaSigArgs { - #[clap(long)] - pub secret_script_key_id: TariKeyId, - #[clap(long)] - pub secret_sender_offset_key_id: TariKeyId, - #[clap(long)] - pub secret_nonce_key_id: TariKeyId, - #[clap(long)] - pub ephemeral_commitment: String, - #[clap(long)] - pub ephemeral_pubkey: String, - #[clap(long)] - pub total_meta_key: UniPublicKey, - #[clap(long)] - pub commitment: String, - #[clap(long)] - pub encrypted_data: String, +pub struct FaucetSpendAggregateUtxoArgs { #[clap(long)] - pub output_features: String, + pub session_id: String, #[clap(long)] - pub recipient_address: TariAddress, + pub input_file_names: Vec, } #[derive(Debug, Args, Clone)] @@ -298,10 +221,8 @@ pub struct MakeItRainArgs { pub increase_amount: MicroMinotari, #[clap(long, parse(try_from_str=parse_start_time))] pub start_time: Option>, - #[clap(short, long)] - pub one_sided: bool, #[clap(long, alias = "stealth-one-sided")] - pub stealth: bool, + pub one_sided: bool, #[clap(short, long)] pub burn_tari: bool, #[clap(short, long, default_value = "Make it rain")] @@ -310,7 +231,7 @@ pub struct MakeItRainArgs { impl MakeItRainArgs { pub fn transaction_type(&self) -> MakeItRainTransactionType { - if self.stealth { + if self.one_sided { MakeItRainTransactionType::StealthOneSided } else if self.burn_tari { MakeItRainTransactionType::BurnTari diff --git a/applications/minotari_console_wallet/src/wallet_modes.rs b/applications/minotari_console_wallet/src/wallet_modes.rs index a35d719f20..841ff135db 100644 --- a/applications/minotari_console_wallet/src/wallet_modes.rs +++ b/applications/minotari_console_wallet/src/wallet_modes.rs @@ -144,14 +144,12 @@ pub(crate) fn command_mode( wallet: WalletSqlite, command: CliCommands, ) -> Result<(), ExitError> { - let commands = vec![command]; - // Do not remove this println! const CUCUMBER_TEST_MARKER_A: &str = "Minotari Console Wallet running... (Command mode started)"; println!("{}", CUCUMBER_TEST_MARKER_A); info!(target: LOG_TARGET, "Starting wallet command mode"); - handle.block_on(command_runner(config, commands, wallet.clone()))?; + handle.block_on(command_runner(config, vec![command.clone()], wallet.clone()))?; // Do not remove this println! const CUCUMBER_TEST_MARKER_B: &str = "Minotari Console Wallet running... (Command mode completed)"; @@ -159,7 +157,25 @@ pub(crate) fn command_mode( info!(target: LOG_TARGET, "Completed wallet command mode"); - wallet_or_exit(handle, cli, config, base_node_config, wallet) + wallet_or_exit( + handle, + cli, + config, + base_node_config, + wallet, + force_exit_for_faucet_commands(&command), + ) +} + +fn force_exit_for_faucet_commands(command: &CliCommands) -> bool { + matches!( + command, + CliCommands::FaucetGenerateSessionInfo(_) | + CliCommands::FaucetEncumberAggregateUtxo(_) | + CliCommands::FaucetSpendAggregateUtxo(_) | + CliCommands::FaucetCreatePartyDetails(_) | + CliCommands::FaucetCreateInputOutputSigs(_) + ) } pub(crate) fn parse_command_file(script: String) -> Result, ExitError> { @@ -206,22 +222,33 @@ pub(crate) fn script_mode( println!("Parsing commands..."); let commands = parse_command_file(script)?; - println!("{} commands parsed successfully.", commands.len()); + let mut exit_wallet = false; + for command in &commands { + if force_exit_for_faucet_commands(command) { + println!("Faucet commands may not run in script mode!"); + exit_wallet = true; + break; + } + } - // Do not remove this println! - const CUCUMBER_TEST_MARKER_A: &str = "Minotari Console Wallet running... (Script mode started)"; - println!("{}", CUCUMBER_TEST_MARKER_A); + if !exit_wallet { + println!("{} commands parsed successfully.", commands.len()); - println!("Starting the command runner!"); - handle.block_on(command_runner(config, commands, wallet.clone()))?; + // Do not remove this println! + const CUCUMBER_TEST_MARKER_A: &str = "Minotari Console Wallet running... (Script mode started)"; + println!("{}", CUCUMBER_TEST_MARKER_A); - // Do not remove this println! - const CUCUMBER_TEST_MARKER_B: &str = "Minotari Console Wallet running... (Script mode completed)"; - println!("{}", CUCUMBER_TEST_MARKER_B); + println!("Starting the command runner!"); + handle.block_on(command_runner(config, commands, wallet.clone()))?; + + // Do not remove this println! + const CUCUMBER_TEST_MARKER_B: &str = "Minotari Console Wallet running... (Script mode completed)"; + println!("{}", CUCUMBER_TEST_MARKER_B); - info!(target: LOG_TARGET, "Completed wallet script mode"); + info!(target: LOG_TARGET, "Completed wallet script mode"); + } - wallet_or_exit(handle, cli, config, base_node_config, wallet) + wallet_or_exit(handle, cli, config, base_node_config, wallet, exit_wallet) } /// Prompts the user to continue to the wallet, or exit. @@ -231,11 +258,16 @@ fn wallet_or_exit( config: &WalletConfig, base_node_config: &PeerConfig, wallet: WalletSqlite, + force_exit: bool, ) -> Result<(), ExitError> { if cli.command_mode_auto_exit { info!(target: LOG_TARGET, "Auto exit argument supplied - exiting."); return Ok(()); } + if force_exit { + info!(target: LOG_TARGET, "Forced exit argument supplied by process - exiting."); + return Ok(()); + } if cli.non_interactive_mode { info!(target: LOG_TARGET, "Starting GRPC server."); @@ -482,7 +514,8 @@ mod test { #[test] #[allow(clippy::too_many_lines)] fn clap_parses_user_defined_commands_as_expected() { - let script = " + let script = + " # Beginning of script file get-balance @@ -492,68 +525,31 @@ mod test { discover-peer f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 send-minotari --message Our_secret! 125T \ - f425UWsDp714RiN53c1G6ek57rfFnotB5NCMyrn4iDgbR8i2sXVHa4xSsedd66o9KmkRgErQnyDdCaAdNLzcKrj7eUb + f425UWsDp714RiN53c1G6ek57rfFnotB5NCMyrn4iDgbR8i2sXVHa4xSsedd66o9KmkRgErQnyDdCaAdNLzcKrj7eUb burn-minotari --message Ups_these_funds_will_be_burned! 100T - faucet-encumber-aggregate-utxo \ - --fee-per-gram 1 \ - --output-hash f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ - --commitment f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ - --script-input-shares=3ddde10d0775c20fb25015546c6a8068812044e7ca4ee1057e84ec9ab6705d03,8a55d1cb503be36875d38f2dc6abac7b23445bbd7253684a1506f5ee1855cd58 \ - --script-input-shares=3edf1ed103b0ac0bbad6a6de8369808d14dfdaaf294fe660646875d749a1f908,50a26c646db951720c919f59cd7a34600a7fc3ee978c64fbcce0ad184c46844c \ - --script-public-key-shares=5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ - --script-public-key-shares=f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ - --script-signature-public-nonces=8a55d1cb503be36875d38f2dc6abac7b23445bbd7253684a1506f5ee1855cd58 \ - --script-signature-public-nonces=50a26c646db951720c919f59cd7a34600a7fc3ee978c64fbcce0ad184c46844c \ - --sender-offset-public-key-shares=5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ - --sender-offset-public-key-shares=f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ - --metadata-ephemeral-public-key-shares=5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ - --metadata-ephemeral-public-key-shares=f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ - --dh-shared-secret-shares=5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ - --dh-shared-secret-shares=f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ - --recipient-address f4LR9f6WwwcPiKJjK5ciTkU1ocNhANa3FPw1wkyVUwbuKpgiihawCXy6PFszunUWQ4Te8KVFnyWVHHwsk9x5Cg7ZQiA - - faucet-spend-aggregate-utxo \ - --tx-id 12345678 \ - --meta-signatures=3ddde10d0775c20fb25015546c6a8068812044e7ca4ee1057e84ec9ab6705d03,8a55d1cb503be36875d38f2dc6abac7b23445bbd7253684a1506f5ee1855cd58 \ - --meta-signatures=3edf1ed103b0ac0bbad6a6de8369808d14dfdaaf294fe660646875d749a1f908,50a26c646db951720c919f59cd7a34600a7fc3ee978c64fbcce0ad184c46844c \ - --script-signatures=3ddde10d0775c20fb25015546c6a8068812044e7ca4ee1057e84ec9ab6705d03,8a55d1cb503be36875d38f2dc6abac7b23445bbd7253684a1506f5ee1855cd58 \ - --script-signatures=3edf1ed103b0ac0bbad6a6de8369808d14dfdaaf294fe660646875d749a1f908,50a26c646db951720c919f59cd7a34600a7fc3ee978c64fbcce0ad184c46844c \ - --script-offset-keys=5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ - --script-offset-keys=f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 - - faucet-create-party-details \ - --commitment f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ - --recipient-address f4LR9f6WwwcPiKJjK5ciTkU1ocNhANa3FPw1wkyVUwbuKpgiihawCXy6PFszunUWQ4Te8KVFnyWVHHwsk9x5Cg7ZQiA - - faucet-create-script-sig \ - --private-key-id imported.96159b07298a453c9f514f5307f70659c7561dd6d9ed376854c5cb573cb2e311 \ - --secret-nonce-key-id imported.96159b07298a453c9f514f5307f70659c7561dd6d9ed376854c5cb573cb2e311 \ - --input-script ae010268593ed2d36a2d95f0ffe0f41649b97cc36fc4ef0c8ecd6bd28f9d56c76b793b08691435a5c813578f8a7f4973166dc1c6c15f37aec2a7d65b1583c8b2129364c916d5986a0c1b3dac7d6efb94bed688ba52fa8b962cf27c0446e2fea6d66a04 \ - --input-stack 050857c14f72cf885aac9f08c9484cb7cb06b6cc20eab68c9bee1e8d5a85649b0a6d31c5cc49afc1e03ebbcf55c82f47e8cbc796c33e96c17a31eab027ee821f00 \ - --ephemeral-commitment f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ - --ephemeral-pubkey 8a55d1cb503be36875d38f2dc6abac7b23445bbd7253684a1506f5ee1855cd58 \ - --total-script-key 5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ - --commitment 94966b4f1b5dc050df1109cf07a516ae85912c82503b1a8c1625986a569fae67 - - faucet-create-meta-sig \ - --secret-script-key-id imported.96159b07298a453c9f514f5307f70659c7561dd6d9ed376854c5cb573cb2e311 \ - --secret-sender-offset-key-id imported.96159b07298a453c9f514f5307f70659c7561dd6d9ed376854c5cb573cb2e311 \ - --secret-nonce-key-id imported.96159b07298a453c9f514f5307f70659c7561dd6d9ed376854c5cb573cb2e311 \ - --ephemeral-commitment f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ - --ephemeral-pubkey 8a55d1cb503be36875d38f2dc6abac7b23445bbd7253684a1506f5ee1855cd58 \ - --total-meta-key 5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ - --commitment 94966b4f1b5dc050df1109cf07a516ae85912c82503b1a8c1625986a569fae67 \ - --encrypted-data 6a7aa2053ae187f60f27df0e10184bf93d02a84cd9548320ec7da546185fc23c6daa720974007c6106cfb0361eb9828e1af979b69fff724d2bcd0d86d5b9675ef1f65b424b22bee06e52fcaf4fd2a2ed \ - --output-features 'features' \ - --recipient-address f4FB7HhYCmLw4PsivjG8bAgUuxyPS6GTjFkhMWx6d9Nv4aoBESyaH5TdS1dAkSCg4qXqehpjZU9QrSUP2Ec7v4Gj8wf + faucet-generate-session-info --fee-per-gram 2 --commitment \ + f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 --output-hash \ + f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 --recipient-address \ + f4LR9f6WwwcPiKJjK5ciTkU1ocNhANa3FPw1wkyVUwbuKpgiihawCXy6PFszunUWQ4Te8KVFnyWVHHwsk9x5Cg7ZQiA \ + --verify-unspent-outputs + + faucet-create-party-details --session-id ee1643655c --input-file ./step_1_session_info.txt --alias alice + + faucet-encumber-aggregate-utxo --session-id ee1643655c --input-file-names=step_2_for_leader_from_alice.txt \ + --input-file-names=step_2_for_leader_from_bob.txt --input-file-names=step_2_for_leader_from_carol.txt + + faucet-create-input-output-sigs --session-id ee1643655c + + faucet-spend-aggregate-utxo --session-id ee1643655c --input-file-names=step_4_for_leader_from_alice.txt \ + --input-file-names=step_4_for_leader_from_bob.txt --input-file-names=step_4_for_leader_from_carol.txt coin-split --message Make_many_dust_UTXOs! --fee-per-gram 2 0.001T 499 make-it-rain --duration 100 --transactions-per-second 10 --start-amount 0.009200T --increase-amount 0T \ - --start-time now --message Stressing_it_a_bit...!_(from_Feeling-a-bit-Generous) \ - f425UWsDp714RiN53c1G6ek57rfFnotB5NCMyrn4iDgbR8i2sXVHa4xSsedd66o9KmkRgErQnyDdCaAdNLzcKrj7eUb + --start-time now --message Stressing_it_a_bit...!_(from_Feeling-a-bit-Generous) \ + f425UWsDp714RiN53c1G6ek57rfFnotB5NCMyrn4iDgbR8i2sXVHa4xSsedd66o9KmkRgErQnyDdCaAdNLzcKrj7eUb export-tx 123456789 --output-file pie.txt @@ -561,18 +557,18 @@ mod test { # End of script file " - .to_string(); + .to_string(); let commands = parse_command_file(script).unwrap(); let mut get_balance = false; let mut send_tari = false; let mut burn_tari = false; + let mut faucet_generate_session_info = false; let mut faucet_encumber_aggregate_utxo = false; let mut faucet_spend_aggregate_utxo = false; let mut faucet_create_party_details = false; - let mut faucet_create_script_sig = false; - let mut faucet_create_meta_sig = false; + let mut faucet_create_input_output_sigs = false; let mut make_it_rain = false; let mut coin_split = false; let mut discover_peer = false; @@ -584,11 +580,11 @@ mod test { CliCommands::GetBalance => get_balance = true, CliCommands::SendMinotari(_) => send_tari = true, CliCommands::BurnMinotari(_) => burn_tari = true, + CliCommands::FaucetGenerateSessionInfo(_) => faucet_generate_session_info = true, CliCommands::FaucetEncumberAggregateUtxo(_) => faucet_encumber_aggregate_utxo = true, CliCommands::FaucetSpendAggregateUtxo(_) => faucet_spend_aggregate_utxo = true, CliCommands::FaucetCreatePartyDetails(_) => faucet_create_party_details = true, - CliCommands::FaucetCreateScriptSig(_) => faucet_create_script_sig = true, - CliCommands::FaucetCreateMetaSig(_) => faucet_create_meta_sig = true, + CliCommands::FaucetCreateInputOutputSigs(_) => faucet_create_input_output_sigs = true, CliCommands::SendOneSidedToStealthAddress(_) => {}, CliCommands::MakeItRain(_) => make_it_rain = true, CliCommands::CoinSplit(_) => coin_split = true, @@ -622,11 +618,11 @@ mod test { get_balance && send_tari && burn_tari && + faucet_generate_session_info && faucet_encumber_aggregate_utxo && faucet_spend_aggregate_utxo && faucet_create_party_details && - faucet_create_script_sig && - faucet_create_meta_sig && + faucet_create_input_output_sigs && make_it_rain && coin_split && discover_peer && diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index cef456bbae..df1f6ca951 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -64,7 +64,7 @@ pub enum OutputManagerRequest { EncumberAggregateUtxo { tx_id: TxId, fee_per_gram: MicroMinotari, - output_hash: String, + output_hash: HashOutput, expected_commitment: PedersenCommitment, script_input_shares: HashMap, script_signature_public_nonces: Vec, @@ -165,7 +165,7 @@ impl fmt::Display for OutputManagerRequest { "Encumber aggregate utxo with tx_id: {} and output: ({},{})", tx_id, expected_commitment.to_hex(), - output_hash + output_hash.to_hex() ), GetRecipientTransaction(_) => write!(f, "GetRecipientTransaction"), ConfirmPendingTransaction(v) => write!(f, "ConfirmPendingTransaction ({})", v), @@ -763,7 +763,7 @@ impl OutputManagerHandle { &mut self, tx_id: TxId, fee_per_gram: MicroMinotari, - output_hash: String, + output_hash: HashOutput, expected_commitment: PedersenCommitment, script_input_shares: HashMap, script_signature_public_nonces: Vec, diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index aefd16e3e0..39631d191d 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -32,7 +32,7 @@ use tari_common::configuration::Network; use tari_common_types::{ tari_address::TariAddress, transaction::TxId, - types::{BlockHash, Commitment, FixedHash, HashOutput, PrivateKey, PublicKey}, + types::{BlockHash, Commitment, HashOutput, PrivateKey, PublicKey}, }; use tari_comms::{types::CommsDHKE, NodeIdentity}; use tari_core::{ @@ -1179,7 +1179,7 @@ where &mut self, tx_id: TxId, fee_per_gram: MicroMinotari, - output_hash: String, + output_hash: HashOutput, expected_commitment: PedersenCommitment, mut script_input_shares: HashMap, script_signature_public_nonces: Vec, @@ -1203,13 +1203,17 @@ where OutputManagerError, > { // Fetch the output from the blockchain - let output_hash = - FixedHash::from_hex(&output_hash).map_err(|e| OutputManagerError::ConversionError(e.to_string()))?; let output = self .fetch_unspent_outputs_from_node(vec![output_hash]) .await? .pop() - .ok_or_else(|| OutputManagerError::ServiceError(format!("Output not found (TxId: {})", tx_id)))?; + .ok_or_else(|| { + OutputManagerError::ServiceError(format!( + "Output with hash {} not found in blockchain (TxId: {})", + output_hash.to_hex(), + tx_id + )) + })?; if output.commitment != expected_commitment { return Err(OutputManagerError::ServiceError(format!( "Output commitment does not match expected commitment (TxId: {})", diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 841c2fdfc7..3a9c4a07ea 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -32,7 +32,7 @@ use tari_common_types::{ burnt_proof::BurntProof, tari_address::TariAddress, transaction::{ImportStatus, TxId}, - types::{FixedHash, PrivateKey, PublicKey, Signature}, + types::{FixedHash, HashOutput, PrivateKey, PublicKey, Signature}, }; use tari_comms::types::CommsPublicKey; use tari_core::{ @@ -113,7 +113,7 @@ pub enum TransactionServiceRequest { }, EncumberAggregateUtxo { fee_per_gram: MicroMinotari, - output_hash: String, + output_hash: HashOutput, expected_commitment: PedersenCommitment, script_input_shares: HashMap, script_signature_public_nonces: Vec, @@ -122,6 +122,9 @@ pub enum TransactionServiceRequest { dh_shared_secret_shares: Vec, recipient_address: TariAddress, }, + FetchUnspentOutputs { + output_hashes: Vec, + }, FinalizeSentAggregateTransaction { tx_id: u64, total_meta_data_signature: Signature, @@ -244,7 +247,7 @@ impl fmt::Display for TransactionServiceRequest { script_input_shares = {:?},, script_signature_shares = {:?}, sender_offset_public_key_shares = {:?}, \ metadata_ephemeral_public_key_shares = {:?}, dh_shared_secret_shares = {:?}, recipient_address = {}", fee_per_gram, - output_hash, + output_hash.to_hex(), expected_commitment.to_hex(), script_input_shares .iter() @@ -273,6 +276,13 @@ impl fmt::Display for TransactionServiceRequest { .collect::>(), recipient_address, )), + Self::FetchUnspentOutputs { output_hashes } => { + write!( + f, + "FetchUnspentOutputs({:?})", + output_hashes.iter().map(|v| v.to_hex()).collect::>() + ) + }, Self::FinalizeSentAggregateTransaction { tx_id, total_meta_data_signature, @@ -358,6 +368,7 @@ pub enum TransactionServiceResponse { TransactionSent(TxId), TransactionSentWithOutputHash(TxId, FixedHash), EncumberAggregateUtxo(TxId, Box, Box, Box, Box), + UnspentOutputs(Vec), TransactionImported(TxId), BurntTransactionSent { tx_id: TxId, @@ -730,7 +741,7 @@ impl TransactionServiceHandle { pub async fn encumber_aggregate_utxo( &mut self, fee_per_gram: MicroMinotari, - output_hash: String, + output_hash: HashOutput, expected_commitment: PedersenCommitment, script_input_shares: HashMap, script_signature_public_nonces: Vec, @@ -771,6 +782,20 @@ impl TransactionServiceHandle { } } + pub async fn fetch_unspent_outputs( + &mut self, + output_hashes: Vec, + ) -> Result, TransactionServiceError> { + match self + .handle + .call(TransactionServiceRequest::FetchUnspentOutputs { output_hashes }) + .await?? + { + TransactionServiceResponse::UnspentOutputs(outputs) => Ok(outputs), + _ => Err(TransactionServiceError::UnexpectedApiResponse), + } + } + pub async fn finalize_aggregate_utxo( &mut self, tx_id: u64, diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index e13e0c5be3..b202632fa1 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -38,7 +38,7 @@ use tari_common_types::{ burnt_proof::BurntProof, tari_address::{TariAddress, TariAddressFeatures}, transaction::{ImportStatus, TransactionDirection, TransactionStatus, TxId}, - types::{CommitmentFactory, FixedHash, PrivateKey, PublicKey, Signature}, + types::{CommitmentFactory, FixedHash, HashOutput, PrivateKey, PublicKey, Signature}, }; use tari_comms::{types::CommsPublicKey, NodeIdentity}; use tari_comms_dht::outbound::OutboundMessageRequester; @@ -52,7 +52,7 @@ use tari_core::{ shared_secret_to_output_spending_key, stealth_address_script_spending_key, }, - proto::base_node as base_node_proto, + proto::{base_node as base_node_proto, base_node::FetchMatchingUtxos}, transactions::{ key_manager::{TariKeyId, TransactionKeyManagerInterface}, tari_amount::MicroMinotari, @@ -753,6 +753,10 @@ where ) }, ), + TransactionServiceRequest::FetchUnspentOutputs { output_hashes } => { + let unspent_outputs = self.fetch_unspent_outputs_from_node(output_hashes).await?; + Ok(TransactionServiceResponse::UnspentOutputs(unspent_outputs)) + }, TransactionServiceRequest::FinalizeSentAggregateTransaction { tx_id, total_meta_data_signature, @@ -1375,12 +1379,40 @@ where Ok((tx_id, output_hash)) } + async fn fetch_unspent_outputs_from_node( + &mut self, + hashes: Vec, + ) -> Result, TransactionServiceError> { + // lets get the output from the blockchain + let req = FetchMatchingUtxos { + output_hashes: hashes.iter().map(|v| v.to_vec()).collect(), + }; + let results: Vec = self + .resources + .connectivity + .obtain_base_node_wallet_rpc_client() + .await + .ok_or_else(|| { + TransactionServiceError::ServiceError("Could not connect to base node rpc client".to_string()) + })? + .fetch_matching_utxos(req) + .await? + .outputs + .into_iter() + .filter_map(|o| match o.try_into() { + Ok(output) => Some(output), + _ => None, + }) + .collect(); + Ok(results) + } + /// Creates an encumbered uninitialized transaction #[allow(clippy::mutable_key_type)] pub async fn encumber_aggregate_tx( &mut self, fee_per_gram: MicroMinotari, - output_hash: String, + output_hash: HashOutput, expected_commitment: PedersenCommitment, script_input_shares: HashMap, script_signature_public_nonces: Vec, diff --git a/base_layer/wallet_ffi/wallet.h b/base_layer/wallet_ffi/wallet.h index 321b19a998..576a0cc4bf 100644 --- a/base_layer/wallet_ffi/wallet.h +++ b/base_layer/wallet_ffi/wallet.h @@ -640,7 +640,7 @@ struct ByteVector *public_key_get_bytes(TariPublicKey *pk, int *error_out); /** - * Converts public key to emoji encding + * Converts public key to emoji encoding * * ## Arguments * `pk` - The pointer to a TariPublicKey diff --git a/integration_tests/tests/steps/wallet_cli_steps.rs b/integration_tests/tests/steps/wallet_cli_steps.rs index 1cf180fb77..4c89339db9 100644 --- a/integration_tests/tests/steps/wallet_cli_steps.rs +++ b/integration_tests/tests/steps/wallet_cli_steps.rs @@ -226,7 +226,6 @@ async fn make_it_rain( destination: wallet_b_address, start_time: None, one_sided: false, - stealth: false, burn_tari: false, };