diff --git a/Cargo.lock b/Cargo.lock index 4692d2582f..ffb0bb2560 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3304,7 +3304,6 @@ dependencies = [ "serde_json", "sha2 0.10.8", "strum", - "strum_macros", "tari_common", "tari_common_types", "tari_comms", diff --git a/applications/minotari_app_utilities/src/utilities.rs b/applications/minotari_app_utilities/src/utilities.rs index e1e708bb25..afbb03bea2 100644 --- a/applications/minotari_app_utilities/src/utilities.rs +++ b/applications/minotari_app_utilities/src/utilities.rs @@ -28,10 +28,10 @@ use tari_common::exit_codes::{ExitCode, ExitError}; use tari_common_types::{ emoji::EmojiId, tari_address::TariAddress, - types::{BlockHash, PublicKey}, + types::{BlockHash, PrivateKey, PublicKey, Signature}, }; use tari_comms::{peer_manager::NodeId, types::CommsPublicKey}; -use tari_utilities::hex::Hex; +use tari_utilities::hex::{Hex, HexError}; use thiserror::Error; use tokio::{runtime, runtime::Runtime}; @@ -95,7 +95,7 @@ impl From for PublicKey { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum UniNodeId { PublicKey(PublicKey), NodeId(NodeId), @@ -140,6 +140,28 @@ impl TryFrom for PublicKey { } } +#[derive(Debug, Clone)] +pub struct UniSignature(Signature); + +impl FromStr for UniSignature { + type Err = HexError; + + fn from_str(s: &str) -> Result { + let data = s.split(',').collect::>(); + let signature = PrivateKey::from_hex(data[0])?; + let public_nonce = PublicKey::from_hex(data[1])?; + + let signature = Signature::new(public_nonce, signature); + Ok(Self(signature)) + } +} + +impl From for Signature { + fn from(id: UniSignature) -> Self { + id.0 + } +} + impl From for NodeId { fn from(id: UniNodeId) -> Self { match id { diff --git a/applications/minotari_console_wallet/Cargo.toml b/applications/minotari_console_wallet/Cargo.toml index be2b2a96c2..44674b02b2 100644 --- a/applications/minotari_console_wallet/Cargo.toml +++ b/applications/minotari_console_wallet/Cargo.toml @@ -64,7 +64,6 @@ serde = "1.0.136" serde_json = "1.0.79" sha2 = "0.10" strum = "0.22" -strum_macros = "0.22" thiserror = "1.0.26" tonic = "0.8.3" unicode-segmentation = "1.6.0" diff --git a/applications/minotari_console_wallet/src/automation/commands.rs b/applications/minotari_console_wallet/src/automation/commands.rs index aa459859d5..1604e816f6 100644 --- a/applications/minotari_console_wallet/src/automation/commands.rs +++ b/applications/minotari_console_wallet/src/automation/commands.rs @@ -48,13 +48,12 @@ use minotari_wallet::{ }; use serde::{de::DeserializeOwned, Serialize}; use sha2::Sha256; -use strum_macros::{Display, EnumIter, EnumString}; use tari_common_types::{ burnt_proof::BurntProof, emoji::EmojiId, tari_address::TariAddress, transaction::TxId, - types::{Commitment, FixedHash, PublicKey, Signature}, + types::{Commitment, FixedHash, PrivateKey, PublicKey, Signature}, }; use tari_comms::{ connectivity::{ConnectivityEvent, ConnectivityRequester}, @@ -62,17 +61,28 @@ use tari_comms::{ types::CommsPublicKey, }; use tari_comms_dht::{envelope::NodeDestination, DhtDiscoveryRequester}; -use tari_core::transactions::{ - tari_amount::{uT, MicroMinotari, Minotari}, - transaction_components::{ - encrypted_data::PaymentId, - OutputFeatures, - TransactionOutput, - UnblindedOutput, - WalletOutput, +use tari_core::{ + covenants::Covenant, + transactions::{ + key_manager::TransactionKeyManagerInterface, + tari_amount::{uT, MicroMinotari, Minotari}, + transaction_components::{ + encrypted_data::PaymentId, + EncryptedData, + OutputFeatures, + RangeProofType, + Transaction, + TransactionInput, + TransactionInputVersion, + TransactionOutput, + TransactionOutputVersion, + UnblindedOutput, + WalletOutput, + }, }, }; use tari_crypto::ristretto::RistrettoSecretKey; +use tari_script::{script, ExecutionStack, TariScript}; use tari_utilities::{hex::Hex, ByteArray}; use tokio::{ sync::{broadcast, mpsc}, @@ -87,34 +97,6 @@ use crate::{ pub const LOG_TARGET: &str = "wallet::automation::commands"; -/// Enum representing commands used by the wallet -#[derive(Clone, PartialEq, Debug, Display, EnumIter, EnumString)] -#[strum(serialize_all = "kebab_case")] -pub enum WalletCommand { - GetBalance, - SendTari, - SendOneSided, - MakeItRain, - CoinSplit, - DiscoverPeer, - Whois, - ExportUtxos, - ExportTx, - ImportTx, - ExportSpentUtxos, - CountUtxos, - SetBaseNode, - SetCustomBaseNode, - ClearCustomBaseNode, - InitShaAtomicSwap, - FinaliseShaAtomicSwap, - ClaimShaAtomicSwapRefund, - RegisterAsset, - MintTokens, - CreateInitialCheckpoint, - RevalidateWalletDb, -} - #[derive(Debug)] pub struct SentTransaction {} @@ -157,6 +139,86 @@ pub async fn burn_tari( .map_err(CommandError::TransactionServiceError) } +pub async fn create_aggregate_signature_utxo( + mut wallet_transaction_service: TransactionServiceHandle, + amount: MicroMinotari, + fee_per_gram: MicroMinotari, + n: u8, + m: u8, + public_keys: Vec, + message: String, + maturity: u64, +) -> Result<(TxId, FixedHash), CommandError> { + let mut msg = [0u8; 32]; + msg.copy_from_slice(message.as_bytes()); + + wallet_transaction_service + .create_aggregate_signature_utxo(amount, fee_per_gram, n, m, public_keys, msg, maturity) + .await + .map_err(CommandError::TransactionServiceError) +} + +/// encumbers a n-of-m transaction +#[allow(clippy::too_many_arguments)] +async fn encumber_aggregate_utxo( + mut wallet_transaction_service: TransactionServiceHandle, + fee_per_gram: MicroMinotari, + output_hash: String, + script_input_shares: Vec, + script_public_key_shares: Vec, + script_signature_shares: Vec, + sender_offset_public_key_shares: Vec, + metadata_ephemeral_public_key_shares: Vec, + dh_shared_secret_shares: Vec, + recipient_address: TariAddress, + payment_id: PaymentId, + maturity: u64, + range_proof_type: RangeProofType, + minimum_value_promise: MicroMinotari, +) -> Result<(TxId, Transaction, PublicKey), CommandError> { + wallet_transaction_service + .encumber_aggregate_utxo( + fee_per_gram, + output_hash, + script_input_shares, + script_public_key_shares, + script_signature_shares, + sender_offset_public_key_shares, + metadata_ephemeral_public_key_shares, + dh_shared_secret_shares, + recipient_address, + payment_id, + maturity, + range_proof_type, + minimum_value_promise, + ) + .await + .map_err(CommandError::TransactionServiceError) +} + +/// finalises an already encumbered a n-of-m transaction +async fn finalise_aggregate_utxo( + mut wallet_transaction_service: TransactionServiceHandle, + tx_id: u64, + meta_signatures: Vec, + script_signatures: Vec, + wallet_script_secret_key: PrivateKey, +) -> Result { + let mut meta_sig = Signature::default(); + for sig in &meta_signatures { + meta_sig = &meta_sig + sig; + } + let mut script_sig = Signature::default(); + for sig in &script_signatures { + script_sig = &script_sig + sig; + } + + wallet_transaction_service + .finalize_aggregate_utxo(tx_id, meta_sig, script_sig, wallet_script_secret_key) + .await + .map_err(CommandError::TransactionServiceError) +} + /// publishes a tari-SHA atomic swap HTLC transaction pub async fn init_sha_atomic_swap( mut wallet_transaction_service: TransactionServiceHandle, @@ -617,6 +679,7 @@ pub async fn command_runner( let mut output_service = wallet.output_manager_service.clone(); let dht_service = wallet.dht_service.discovery_service_requester().clone(); let connectivity_requester = wallet.comms.connectivity(); + let key_manager_service = wallet.key_manager_service.clone(); let mut online = false; let mut tx_ids = Vec::new(); @@ -676,6 +739,273 @@ pub async fn command_runner( Err(e) => eprintln!("BurnMinotari error! {}", e), } }, + CreateKeyPair(args) => match key_manager_service.create_key_pair(args.key_branch).await { + Ok((key_id, pk)) => { + println!( + "New key pair: + 1. key id : {}, + 2. public key: {}", + key_id, + pk.to_hex() + ) + }, + Err(e) => eprintln!("CreateKeyPair error! {}", e), + }, + CreateAggregateSignatureUtxo(args) => match create_aggregate_signature_utxo( + transaction_service.clone(), + args.amount, + args.fee_per_gram, + args.n, + args.m, + args.public_keys + .iter() + .map(|pk| PublicKey::from(pk.clone())) + .collect::>(), + args.message, // 1. What is the message? => commitment + args.maturity, + ) + .await + { + Ok((tx_id, output_hash)) => { + println!( + "Created an utxo with n-of-m aggregate public key, with: + 1. n = {}, + 2. m = {}, + 3. tx id = {}, + 4. output hash = {}", + args.n, args.m, tx_id, output_hash + ) + }, + Err(e) => eprintln!("CreateAggregateSignatureUtxo error! {}", e), + }, + SignMessage(args) => { + match key_manager_service + .sign_message(&args.private_key_id, args.challenge.as_bytes()) + .await + { + // 1. What is the message/challenge? => commitment + Ok(sgn) => { + println!( + "Sign message: + 1. signature: {}, + 2. public nonce: {}", + sgn.get_signature().to_hex(), + sgn.get_public_nonce().to_hex(), + ) + }, + Err(e) => eprintln!("SignMessage error! {}", e), + } + }, + EncumberAggregateUtxo(args) => { + match encumber_aggregate_utxo( + transaction_service.clone(), + args.fee_per_gram, + args.output_hash, + args.script_input_shares + .iter() + .map(|v| v.clone().into()) + .collect::>(), + args.script_public_key_shares + .iter() + .map(|v| v.clone().into()) + .collect::>(), + args.script_signature_shares + .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, + PaymentId::from_bytes(args.payment_id.as_bytes()) + .map_err(|e| CommandError::InvalidArgument(e.to_string()))?, + args.maturity, + args.range_proof_type, + args.minimum_value_promise, + ) + .await + { + Ok((tx_id, transaction, script_pubkey)) => { + println!( + "Encumber 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: {}", + 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] + .script_signature + .ephemeral_commitment() + .to_hex(), + transaction.body.inputs()[0] + .script_signature + .ephemeral_pubkey() + .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] + .metadata_signature + .ephemeral_commitment() + .to_hex(), + transaction.body.outputs()[0] + .metadata_signature + .ephemeral_pubkey() + .to_hex(), + transaction.script_offset.to_hex(), + ) + }, + Err(e) => println!("Encumber aggregate transaction error! {}", e), + } + }, + SpendAggregateUtxo(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; + } + + 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), + } + }, + CreateScriptSig(args) => { + let private_nonce = PrivateKey::from_hex(&args.secret_nonce) + .map_err(|e| CommandError::InvalidArgument(e.to_string()))?; + 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_hex(&args.ephemeral_pubkey) + .map_err(|e| CommandError::InvalidArgument(e.to_string()))?; + let challenge = TransactionInput::build_script_signature_challenge( + &TransactionInputVersion::get_current_version(), + &ephemeral_commitment, + &ephemeral_pubkey, + &script, + &input_data, + &args.total_script_key.into(), + &commitment, + ); + + match key_manager_service + .sign_with_nonce_and_message(&args.private_key_id, &private_nonce, challenge.as_slice()) + .await + { + Ok(signature) => { + println!( + "Sign script sig: + 1. signature: {}, + 2. public nonce: {}", + signature.get_signature().to_hex(), + signature.get_public_nonce().to_hex(), + ) + }, + Err(e) => eprintln!("SignMessage error! {}", e), + } + }, + CreateMetaSig(args) => { + let private_key = PrivateKey::from_hex(&args.secret_offset_key) + .map_err(|e| CommandError::InvalidArgument(e.to_string()))?; + let private_script_key = PrivateKey::from_hex(&args.secret_script_key) + .map_err(|e| CommandError::InvalidArgument(e.to_string()))?; + let private_nonce = PrivateKey::from_hex(&args.secret_nonce) + .map_err(|e| CommandError::InvalidArgument(e.to_string()))?; + let offset = private_script_key - &private_key; + let script = script!(Nop); + let commitment = + Commitment::from_hex(&args.commitment).map_err(|e| CommandError::InvalidArgument(e.to_string()))?; + let covenant = Covenant::default(); + let encrypted_data = EncryptedData::default(); + let output_features = OutputFeatures::default(); + 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, + ); + trace!(target: LOG_TARGET, "meta challange: {:?}", challenge); + // TODO: Change to `ComAndPubSignature` + let signature = Signature::sign_with_nonce_and_message(&private_key, private_nonce, challenge) + .map_err(CommandError::FailedSignature)?; + println!( + "Sign meta sig: + 1. signature: {}, + 2. public nonce: {}, + Script offset: {}", + signature.get_signature().to_hex(), + signature.get_public_nonce().to_hex(), + offset.to_hex(), + ) + }, SendMinotari(args) => { match send_tari( transaction_service.clone(), diff --git a/applications/minotari_console_wallet/src/automation/error.rs b/applications/minotari_console_wallet/src/automation/error.rs index 395e59992b..10887d3588 100644 --- a/applications/minotari_console_wallet/src/automation/error.rs +++ b/applications/minotari_console_wallet/src/automation/error.rs @@ -35,6 +35,7 @@ use minotari_wallet::{ use tari_common::exit_codes::{ExitCode, ExitError}; use tari_common_types::types::FixedHashSizeError; use tari_core::transactions::{tari_amount::MicroMinotariError, transaction_components::TransactionError}; +use tari_crypto::signatures::SchnorrSignatureError; use tari_key_manager::key_manager_service::KeyManagerServiceError; use tari_utilities::{hex::HexError, ByteArrayError}; use thiserror::Error; @@ -87,6 +88,8 @@ pub enum CommandError { ByteArrayError(String), #[error("gRPC TLS cert error {0}")] GrpcTlsError(#[from] GrpcTlsError), + #[error("Invalid signature: `{0}`")] + FailedSignature(#[from] SchnorrSignatureError), } impl From for CommandError { diff --git a/applications/minotari_console_wallet/src/cli.rs b/applications/minotari_console_wallet/src/cli.rs index 3a4ce8c5d7..2aefd1392f 100644 --- a/applications/minotari_console_wallet/src/cli.rs +++ b/applications/minotari_console_wallet/src/cli.rs @@ -28,11 +28,19 @@ use std::{ use chrono::{DateTime, Utc}; use clap::{Args, Parser, Subcommand}; -use minotari_app_utilities::{common_cli_args::CommonCliArgs, utilities::UniPublicKey}; +use minotari_app_utilities::{ + common_cli_args::CommonCliArgs, + utilities::{UniPublicKey, UniSignature}, +}; use tari_common::configuration::{ConfigOverrideProvider, Network}; use tari_common_types::tari_address::TariAddress; use tari_comms::multiaddr::Multiaddr; -use tari_core::transactions::{tari_amount, tari_amount::MicroMinotari}; +use tari_core::transactions::{ + key_manager::TariKeyId, + tari_amount, + tari_amount::MicroMinotari, + transaction_components::RangeProofType, +}; use tari_key_manager::SeedWords; use tari_utilities::{ hex::{Hex, HexError}, @@ -116,6 +124,13 @@ pub enum CliCommands { GetBalance, SendMinotari(SendMinotariArgs), BurnMinotari(BurnMinotariArgs), + CreateKeyPair(CreateKeyPairArgs), + CreateAggregateSignatureUtxo(CreateAggregateSignatureUtxoArgs), + EncumberAggregateUtxo(EncumberAggregateUtxoArgs), + SpendAggregateUtxo(SpendAggregateUtxoArgs), + SignMessage(SignMessageArgs), + CreateScriptSig(CreateScriptSigArgs), + CreateMetaSig(CreateMetaSigArgs), SendOneSidedToStealthAddress(SendMinotariArgs), MakeItRain(MakeItRainArgs), CoinSplit(CoinSplitArgs), @@ -157,6 +172,118 @@ pub struct BurnMinotariArgs { pub message: String, } +#[derive(Debug, Args, Clone)] +pub struct CreateKeyPairArgs { + #[clap(long)] + pub key_branch: String, +} + +#[derive(Debug, Args, Clone)] +pub struct CreateAggregateSignatureUtxoArgs { + #[clap(long)] + pub amount: MicroMinotari, + #[clap(long)] + pub fee_per_gram: MicroMinotari, + #[clap(long)] + pub n: u8, + #[clap(long)] + pub m: u8, + #[clap(long)] + pub message: String, + #[clap(long)] + pub maturity: u64, + #[clap(long)] + pub public_keys: Vec, +} + +#[derive(Debug, Args, Clone)] +pub struct SignMessageArgs { + #[clap(long)] + pub private_key_id: TariKeyId, + #[clap(long)] + pub challenge: String, +} + +#[derive(Debug, Args, Clone)] +pub struct EncumberAggregateUtxoArgs { + #[clap(long)] + pub fee_per_gram: MicroMinotari, + #[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_shares: 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, + #[clap(long)] + pub recipient_address: TariAddress, + #[clap(long)] + pub payment_id: String, + #[clap(long)] + pub maturity: u64, + #[clap(long)] + pub range_proof_type: RangeProofType, + #[clap(long)] + pub minimum_value_promise: MicroMinotari, +} + +#[derive(Debug, Args, Clone)] +pub struct SpendAggregateUtxoArgs { + #[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, +} + +#[derive(Debug, Args, Clone)] +pub struct CreateScriptSigArgs { + #[clap(long)] + pub private_key_id: TariKeyId, + #[clap(long)] + pub secret_nonce: String, + #[clap(long)] + pub input_script: String, + #[clap(long)] + pub input_stack: String, + #[clap(long)] + pub ephemeral_commitment: String, + #[clap(long)] + pub ephemeral_pubkey: String, + #[clap(long)] + pub total_script_key: UniPublicKey, + #[clap(long)] + pub commitment: String, +} + +#[derive(Debug, Args, Clone)] +pub struct CreateMetaSigArgs { + #[clap(long)] + pub secret_script_key: String, + #[clap(long)] + pub secret_offset_key: String, + #[clap(long)] + pub secret_nonce: String, + #[clap(long)] + pub ephemeral_commitment: String, + #[clap(long)] + pub ephemeral_pubkey: String, + #[clap(long)] + pub total_meta_key: UniPublicKey, + #[clap(long)] + pub commitment: String, +} + #[derive(Debug, Args, Clone)] pub struct MakeItRainArgs { pub destination: TariAddress, diff --git a/applications/minotari_console_wallet/src/wallet_modes.rs b/applications/minotari_console_wallet/src/wallet_modes.rs index 91ad2c3086..ad3a3acab7 100644 --- a/applications/minotari_console_wallet/src/wallet_modes.rs +++ b/applications/minotari_console_wallet/src/wallet_modes.rs @@ -480,6 +480,7 @@ mod test { use crate::{cli::CliCommands, wallet_modes::parse_command_file}; #[test] + #[allow(clippy::too_many_lines)] fn clap_parses_user_defined_commands_as_expected() { let script = " # Beginning of script file @@ -495,6 +496,71 @@ mod test { burn-minotari --message Ups_these_funds_will_be_burned! 100T + create-key-pair --key-branch pie + + create-aggregate-signature-utxo \ + --amount 125T \ + --fee-per-gram 1 \ + --n 3 \ + --m 2 \ + --message ff \ + --maturity 0 \ + --public-keys=5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ + --public-keys=f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 + + sign-message \ + --private-key-id imported.96159b07298a453c9f514f5307f70659c7561dd6d9ed376854c5cb573cb2e311 \ + --challenge f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 + + encumber-aggregate-utxo \ + --fee-per-gram 1 \ + --output-hash f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ + --script-input-shares=3ddde10d0775c20fb25015546c6a8068812044e7ca4ee1057e84ec9ab6705d03,8a55d1cb503be36875d38f2dc6abac7b23445bbd7253684a1506f5ee1855cd58 \ + --script-input-shares=3edf1ed103b0ac0bbad6a6de8369808d14dfdaaf294fe660646875d749a1f908,50a26c646db951720c919f59cd7a34600a7fc3ee978c64fbcce0ad184c46844c \ + --script-public-key-shares=5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ + --script-public-key-shares=f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ + --script-signature-shares=3ddde10d0775c20fb25015546c6a8068812044e7ca4ee1057e84ec9ab6705d03,8a55d1cb503be36875d38f2dc6abac7b23445bbd7253684a1506f5ee1855cd58 \ + --script-signature-shares=3edf1ed103b0ac0bbad6a6de8369808d14dfdaaf294fe660646875d749a1f908,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 \ + --payment-id 156486946518564 \ + --maturity 0 \ + --range-proof-type revealed_value \ + --minimum-value-promise 0 + + 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 + + create-script-sig \ + --private-key-id imported.96159b07298a453c9f514f5307f70659c7561dd6d9ed376854c5cb573cb2e311 \ + --secret-nonce 3ddde10d0775c20fb25015546c6a8068812044e7ca4ee1057e84ec9ab6705d03 \ + --input-script ae010268593ed2d36a2d95f0ffe0f41649b97cc36fc4ef0c8ecd6bd28f9d56c76b793b08691435a5c813578f8a7f4973166dc1c6c15f37aec2a7d65b1583c8b2129364c916d5986a0c1b3dac7d6efb94bed688ba52fa8b962cf27c0446e2fea6d66a04 \ + --input-stack 050857c14f72cf885aac9f08c9484cb7cb06b6cc20eab68c9bee1e8d5a85649b0a6d31c5cc49afc1e03ebbcf55c82f47e8cbc796c33e96c17a31eab027ee821f00 \ + --ephemeral-commitment f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ + --ephemeral-pubkey 8a55d1cb503be36875d38f2dc6abac7b23445bbd7253684a1506f5ee1855cd58 \ + --total-script-key 5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ + --commitment 94966b4f1b5dc050df1109cf07a516ae85912c82503b1a8c1625986a569fae67 + + create-meta-sig \ + --secret-script-key 2603fed9cf87097105913096da423ae4e3096e44a172185742ce5bc00d27016cd81118 \ + --secret-offset-key 3ddde10d0775c20fb25015546c6a8068812044e7ca4ee1057e84ec9ab6705d03 \ + --secret-nonce 3edf1ed103b0ac0bbad6a6de8369808d14dfdaaf294fe660646875d749a1f908 \ + --ephemeral-commitment f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ + --ephemeral-pubkey 8a55d1cb503be36875d38f2dc6abac7b23445bbd7253684a1506f5ee1855cd58 \ + --total-meta-key 5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ + --commitment 94966b4f1b5dc050df1109cf07a516ae85912c82503b1a8c1625986a569fae67 + 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 \ @@ -514,6 +580,13 @@ mod test { let mut get_balance = false; let mut send_tari = false; let mut burn_tari = false; + let mut create_key_pair = false; + let mut create_aggregate_signature_utxo = false; + let mut encumber_aggregate_utxo = false; + let mut spend_aggregate_utxo = false; + let mut sign_message = false; + let mut create_script_sig = false; + let mut create_meta_sig = false; let mut make_it_rain = false; let mut coin_split = false; let mut discover_peer = false; @@ -525,6 +598,13 @@ mod test { CliCommands::GetBalance => get_balance = true, CliCommands::SendMinotari(_) => send_tari = true, CliCommands::BurnMinotari(_) => burn_tari = true, + CliCommands::CreateKeyPair(_) => create_key_pair = true, + CliCommands::CreateAggregateSignatureUtxo(_) => create_aggregate_signature_utxo = true, + CliCommands::EncumberAggregateUtxo(_) => encumber_aggregate_utxo = true, + CliCommands::SpendAggregateUtxo(_) => spend_aggregate_utxo = true, + CliCommands::SignMessage(_) => sign_message = true, + CliCommands::CreateScriptSig(_) => create_script_sig = true, + CliCommands::CreateMetaSig(_) => create_meta_sig = true, CliCommands::SendOneSidedToStealthAddress(_) => {}, CliCommands::MakeItRain(_) => make_it_rain = true, CliCommands::CoinSplit(_) => coin_split = true, @@ -558,6 +638,13 @@ mod test { get_balance && send_tari && burn_tari && + create_key_pair && + create_aggregate_signature_utxo && + encumber_aggregate_utxo && + spend_aggregate_utxo && + sign_message && + create_script_sig && + create_meta_sig && make_it_rain && coin_split && discover_peer && diff --git a/applications/minotari_ledger_wallet/comms/src/ledger_wallet.rs b/applications/minotari_ledger_wallet/comms/src/ledger_wallet.rs index 297b5617ac..48548db056 100644 --- a/applications/minotari_ledger_wallet/comms/src/ledger_wallet.rs +++ b/applications/minotari_ledger_wallet/comms/src/ledger_wallet.rs @@ -39,6 +39,7 @@ pub enum Instruction { GetScriptSignature = 0x05, GetScriptOffset = 0x06, GetMetadataSignature = 0x07, + GetScriptSignatureFromChallenge = 0x08, } impl Instruction { diff --git a/applications/minotari_ledger_wallet/wallet/src/handlers/get_script_signature_from_challenge.rs b/applications/minotari_ledger_wallet/wallet/src/handlers/get_script_signature_from_challenge.rs new file mode 100644 index 0000000000..1ba58fe0fb --- /dev/null +++ b/applications/minotari_ledger_wallet/wallet/src/handlers/get_script_signature_from_challenge.rs @@ -0,0 +1,82 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use alloc::format; + +use ledger_device_sdk::{io::Comm, random::Random, ui::gadgets::SingleMessage}; +use tari_crypto::{ + commitment::HomomorphicCommitmentFactory, + keys::PublicKey, + ristretto::{ + pedersen::{extended_commitment_factory::ExtendedPedersenCommitmentFactory, PedersenCommitment}, + RistrettoComAndPubSig, + RistrettoPublicKey, + RistrettoSecretKey, + }, +}; +use zeroize::Zeroizing; + +use crate::{ + alloc::string::ToString, + utils::{alpha_hasher, derive_from_bip32_key, get_key_from_canonical_bytes}, + AppSW, + KeyType, + RESPONSE_VERSION, + STATIC_ALPHA_INDEX, +}; + +pub fn handler_get_script_signature_from_challenge(comm: &mut Comm) -> Result<(), AppSW> { + let data = comm.get_data().map_err(|_| AppSW::WrongApduLength)?; + + let mut account_bytes = [0u8; 8]; + account_bytes.clone_from_slice(&data[0..8]); + let account = u64::from_le_bytes(account_bytes); + + let alpha = derive_from_bip32_key(account, STATIC_ALPHA_INDEX, KeyType::Alpha)?; + let blinding_factor: Zeroizing = + get_key_from_canonical_bytes::(&data[8..40])?.into(); + let script_private_key = alpha_hasher(alpha, blinding_factor)?; + let script_public_key = RistrettoPublicKey::from_secret_key(&script_private_key); + + let value: Zeroizing = + get_key_from_canonical_bytes::(&data[40..72])?.into(); + let spend_private_key: Zeroizing = + get_key_from_canonical_bytes::(&data[72..104])?.into(); + + let commitment: PedersenCommitment = get_key_from_canonical_bytes(&data[104..136])?; + + let mut challenge = [0u8; 64]; + challenge.clone_from_slice(&data[136..200]); + + let r_a: Zeroizing = + get_key_from_canonical_bytes::(&data[200..232])?.into(); + let r_x: Zeroizing = + get_key_from_canonical_bytes::(&data[232..264])?.into(); + let r_y: Zeroizing = + get_key_from_canonical_bytes::(&data[264..296])?.into(); + + let factory = ExtendedPedersenCommitmentFactory::default(); + + let script_signature = match RistrettoComAndPubSig::sign( + &value, + &spend_private_key, + &script_private_key, + &r_a, + &r_x, + &r_y, + &challenge, + &factory, + ) { + Ok(sig) => sig, + Err(e) => { + SingleMessage::new(&format!("Signing error: {:?}", e.to_string())).show_and_wait(); + return Err(AppSW::ScriptSignatureFail); + }, + }; + + comm.append(&[RESPONSE_VERSION]); // version + comm.append(&script_signature.to_vec()); + comm.reply_ok(); + + Ok(()) +} diff --git a/applications/minotari_ledger_wallet/wallet/src/main.rs b/applications/minotari_ledger_wallet/wallet/src/main.rs index f8527b02ad..19e400c55f 100644 --- a/applications/minotari_ledger_wallet/wallet/src/main.rs +++ b/applications/minotari_ledger_wallet/wallet/src/main.rs @@ -28,6 +28,7 @@ use app_ui::menu::ui_menu_main; use critical_section::RawRestoreState; use handlers::{ get_metadata_signature::handler_get_metadata_signature, + get_metadata_signature::handler_get_script_signature_from_challenge, get_public_alpha::handler_get_public_alpha, get_public_key::handler_get_public_key, get_script_offset::{handler_get_script_offset, ScriptOffsetCtx}, @@ -114,6 +115,7 @@ pub enum Instruction { GetScriptSignature, GetScriptOffset { chunk: u8, more: bool }, GetMetadataSignature, + GetScriptSignatureFromChallenge, } const P2_MORE: u8 = 0x01; @@ -168,6 +170,7 @@ impl TryFrom for Instruction { more: value.p2 == P2_MORE, }), (7, 0, 0) => Ok(Instruction::GetMetadataSignature), + (8, 0, 0) => Ok(Instruction::GetScriptSignatureFromChallenge), (6, _, _) => Err(AppSW::WrongP1P2), (_, _, _) => Err(AppSW::InsNotSupported), } @@ -214,5 +217,6 @@ fn handle_apdu(comm: &mut Comm, ins: Instruction, offset_ctx: &mut ScriptOffsetC Instruction::GetScriptSignature => handler_get_script_signature(comm), Instruction::GetScriptOffset { chunk, more } => handler_get_script_offset(comm, chunk, more, offset_ctx), Instruction::GetMetadataSignature => handler_get_metadata_signature(comm), + Instruction::GetScriptSignatureFromChallenge => handler_get_script_signature_from_challenge(comm), } } diff --git a/base_layer/core/src/common/one_sided.rs b/base_layer/core/src/common/one_sided.rs index 34958668ac..752af555f4 100644 --- a/base_layer/core/src/common/one_sided.rs +++ b/base_layer/core/src/common/one_sided.rs @@ -30,7 +30,7 @@ use tari_crypto::{ keys::{PublicKey as PKtrait, SecretKey as SKtrait}, }; use tari_hashing::WalletOutputEncryptionKeysDomain; -use tari_utilities::byte_array::ByteArrayError; +use tari_utilities::{byte_array::ByteArrayError, ByteArray}; hash_domain!( WalletOutputRewindKeysDomain, @@ -57,6 +57,26 @@ pub fn shared_secret_to_output_encryption_key(shared_secret: &CommsDHKE) -> Resu ) } +/// Generate an output encryption key from a secret key +pub fn secret_key_to_output_encryption_key(secret_key: &PrivateKey) -> Result { + PrivateKey::from_uniform_bytes( + WalletOutputEncryptionKeysDomainHasher::new() + .chain(secret_key.as_bytes()) + .finalize() + .as_ref(), + ) +} + +/// Generate an output encryption key from a public key +pub fn public_key_to_output_encryption_key(public_key: &PublicKey) -> Result { + PrivateKey::from_uniform_bytes( + WalletOutputEncryptionKeysDomainHasher::new() + .chain(public_key.as_bytes()) + .finalize() + .as_ref(), + ) +} + /// Generate an output spending key from a Diffie-Hellman shared secret pub fn shared_secret_to_output_spending_key(shared_secret: &CommsDHKE) -> Result { PrivateKey::from_uniform_bytes( diff --git a/base_layer/core/src/transactions/aggregated_body.rs b/base_layer/core/src/transactions/aggregated_body.rs index 203b43a3d7..2a8bc7bb56 100644 --- a/base_layer/core/src/transactions/aggregated_body.rs +++ b/base_layer/core/src/transactions/aggregated_body.rs @@ -27,8 +27,9 @@ use std::{ use borsh::{BorshDeserialize, BorshSerialize}; use log::*; use serde::{Deserialize, Serialize}; -use tari_common_types::types::{Commitment, PrivateKey}; +use tari_common_types::types::{ComAndPubSignature, Commitment, PrivateKey}; use tari_crypto::commitment::HomomorphicCommitmentFactory; +use tari_utilities::hex::Hex; use crate::transactions::{ crypto_factories::CryptoFactories, @@ -104,11 +105,46 @@ impl AggregateBody { &self.inputs } + /// Update an existing transaction input's script signature (found by matching commitment) + pub fn update_script_signature( + &mut self, + commitment: &Commitment, + script_signature: &ComAndPubSignature, + ) -> Result<(), TransactionError> { + let input = self + .inputs + .iter_mut() + .find(|input| match input.commitment() { + Ok(c) => c == commitment, + Err(_) => false, + }) + .ok_or(TransactionError::OutputNotFound(commitment.to_hex()))?; + input.script_signature = script_signature.clone(); + + Ok(()) + } + /// Provide read-only access to the output list pub fn outputs(&self) -> &Vec { &self.outputs } + /// Update an existing transaction output's metadata signature (found by matching commitment) + pub fn update_metadata_signature( + &mut self, + commitment: &Commitment, + metadata_signature: ComAndPubSignature, + ) -> Result<(), TransactionError> { + let output = self + .outputs + .iter_mut() + .find(|output| &output.commitment == commitment) + .ok_or(TransactionError::OutputNotFound(commitment.to_hex()))?; + output.metadata_signature = metadata_signature; + + Ok(()) + } + /// Provide read-only access to the kernel list pub fn kernels(&self) -> &Vec { &self.kernels @@ -429,7 +465,7 @@ impl Display for AggregateBody { #[cfg(test)] mod test { - use tari_common_types::types::{ComAndPubSignature, FixedHash, PublicKey, Signature}; + use tari_common_types::types::{FixedHash, PublicKey, Signature}; use tari_script::{ExecutionStack, TariScript}; use super::*; diff --git a/base_layer/core/src/transactions/key_manager/inner.rs b/base_layer/core/src/transactions/key_manager/inner.rs index 21a037051c..0c272998e0 100644 --- a/base_layer/core/src/transactions/key_manager/inner.rs +++ b/base_layer/core/src/transactions/key_manager/inner.rs @@ -179,6 +179,12 @@ where TBackend: KeyManagerBackend + 'static Ok((key_id, key)) } + pub async fn create_key_pair(&mut self, branch: &str) -> Result<(TariKeyId, PublicKey), KeyManagerServiceError> { + self.add_key_manager_branch(branch)?; + let (key_id, public_key) = self.get_next_key(branch).await?; + Ok((key_id, public_key)) + } + pub async fn get_static_key(&self, branch: &str) -> Result { match self.key_managers.get(branch) { None => Err(KeyManagerServiceError::UnknownKeyBranch), @@ -665,6 +671,108 @@ where TBackend: KeyManagerBackend + 'static } } + pub async fn get_script_signature_from_challenge( + &self, + script_key_id: &TariKeyId, + spend_key_id: &TariKeyId, + value: &PrivateKey, + challenge: &[u8; 64], + r_a: &PrivateKey, + r_x: &PrivateKey, + r_y: &PrivateKey, + ) -> Result { + let spend_private_key = self.get_private_key(spend_key_id).await?; + + #[allow(unused_variables)] // When ledger isn't enabled + match (&self.wallet_type, script_key_id) { + ( + WalletType::Ledger(ledger), + KeyId::Derived { + branch, + label: _, + index, + }, + ) => { + #[cfg(not(feature = "ledger"))] + { + Err(TransactionError::LedgerNotSupported) + } + + #[cfg(feature = "ledger")] + { + let km = self + .key_managers + .get(branch) + .ok_or(KeyManagerServiceError::UnknownKeyBranch)? + .read() + .await; + let branch_key = km + .get_private_key(*index) + .map_err(|e| TransactionError::KeyManagerError(e.to_string()))?; + + let mut data = branch_key.as_bytes().to_vec(); + data.extend_from_slice(value.as_bytes()); + data.extend_from_slice(spend_private_key.as_bytes()); + data.extend_from_slice(challenge); + data.extend_from_slice(r_a.as_bytes()); + data.extend_from_slice(r_x.as_bytes()); + data.extend_from_slice(r_y.as_bytes()); + + let command = ledger.build_command(Instruction::GetScriptSignatureFromChallenge, data); + let transport = get_transport()?; + + match command.execute_with_transport(&transport) { + Ok(result) => { + if result.data().len() < 161 { + debug!(target: LOG_TARGET, "result less than 161"); + return Err(LedgerDeviceError::Processing(format!( + "'get_script_signature' insufficient data - expected 161 got {} bytes ({:?})", + result.data().len(), + result + )) + .into()); + } + let data = result.data(); + debug!(target: LOG_TARGET, "result length: {}, data: {:?}", result.data().len(), result.data()); + Ok(ComAndPubSignature::new( + Commitment::from_canonical_bytes(&data[1..33]) + .map_err(|e| TransactionError::InvalidSignatureError(e.to_string()))?, + PublicKey::from_canonical_bytes(&data[33..65]) + .map_err(|e| TransactionError::InvalidSignatureError(e.to_string()))?, + PrivateKey::from_canonical_bytes(&data[65..97]) + .map_err(|e| TransactionError::InvalidSignatureError(e.to_string()))?, + PrivateKey::from_canonical_bytes(&data[97..129]) + .map_err(|e| TransactionError::InvalidSignatureError(e.to_string()))?, + PrivateKey::from_canonical_bytes(&data[129..161]) + .map_err(|e| TransactionError::InvalidSignatureError(e.to_string()))?, + )) + }, + Err(e) => Err(LedgerDeviceError::Instruction(format!( + "GetScriptSignatureFromChallenge: {}", + e + )) + .into()), + } + } + }, + (_, _) => { + let script_private_key = self.get_private_key(script_key_id).await?; + + let script_signature = ComAndPubSignature::sign( + value, + &spend_private_key, + &script_private_key, + r_a, + r_x, + r_y, + challenge.as_slice(), + &*self.crypto_factories.commitment, + )?; + Ok(script_signature) + }, + } + } + // ----------------------------------------------------------------------------------------------------------------- // Transaction output section (transactions > transaction_components > transaction_output) // ----------------------------------------------------------------------------------------------------------------- @@ -933,6 +1041,30 @@ where TBackend: KeyManagerBackend + 'static Ok(metadata_signature) } + pub async fn sign_message( + &self, + private_key_id: &TariKeyId, + challenge: &[u8], + ) -> Result { + let private_key = self.get_private_key(private_key_id).await?; + let nonce = PrivateKey::random(&mut OsRng); + let signature = Signature::sign_with_nonce_and_message(&private_key, nonce, challenge)?; + + Ok(signature) + } + + pub async fn sign_with_nonce_and_message( + &self, + private_key_id: &TariKeyId, + nonce: &PrivateKey, + challenge: &[u8], + ) -> Result { + let private_key = self.get_private_key(private_key_id).await?; + let signature = Signature::sign_with_nonce_and_message(&private_key, nonce.clone(), challenge)?; + + Ok(signature) + } + pub async fn get_metadata_signature( &self, spending_key_id: &TariKeyId, diff --git a/base_layer/core/src/transactions/key_manager/interface.rs b/base_layer/core/src/transactions/key_manager/interface.rs index 9697afdc5b..fcf54be1de 100644 --- a/base_layer/core/src/transactions/key_manager/interface.rs +++ b/base_layer/core/src/transactions/key_manager/interface.rs @@ -185,6 +185,17 @@ pub trait TransactionKeyManagerInterface: KeyManagerInterface { script_message: &[u8; 32], ) -> Result; + async fn get_script_signature_from_challenge( + &self, + script_key_id: &TariKeyId, + spend_key_id: &TariKeyId, + value: &PrivateKey, + challenge: &[u8; 64], + r_a: &PrivateKey, + r_x: &PrivateKey, + r_y: &PrivateKey, + ) -> Result; + async fn get_partial_txo_kernel_signature( &self, spend_key_id: &TariKeyId, @@ -247,6 +258,15 @@ pub trait TransactionKeyManagerInterface: KeyManagerInterface { range_proof_type: RangeProofType, ) -> Result; + async fn sign_message(&self, private_key_id: &TariKeyId, challenge: &[u8]) -> Result; + + async fn sign_with_nonce_and_message( + &self, + private_key_id: &TariKeyId, + nonce: &PrivateKey, + challenge: &[u8], + ) -> Result; + async fn get_receiver_partial_metadata_signature( &self, spend_key_id: &TariKeyId, @@ -274,6 +294,11 @@ pub trait TransactionKeyManagerInterface: KeyManagerInterface { amount: &PrivateKey, claim_public_key: &PublicKey, ) -> Result; + + async fn create_key_pair + Send>( + &self, + branch: T, + ) -> Result<(TariKeyId, PublicKey), KeyManagerServiceError>; } #[async_trait::async_trait] @@ -281,3 +306,47 @@ pub trait SecretTransactionKeyManagerInterface: TransactionKeyManagerInterface { /// Gets the pedersen commitment for the specified index async fn get_private_key(&self, key_id: &TariKeyId) -> Result; } + +#[cfg(test)] +mod test { + use core::iter; + use std::str::FromStr; + + use rand::{distributions::Alphanumeric, rngs::OsRng, Rng}; + use tari_common_types::types::{PrivateKey, PublicKey}; + use tari_crypto::keys::{PublicKey as PK, SecretKey as SK}; + + use crate::transactions::key_manager::TariKeyId; + + fn random_string(len: usize) -> String { + iter::repeat(()) + .map(|_| OsRng.sample(Alphanumeric) as char) + .take(len) + .collect() + } + + #[test] + fn key_id_converts_correctly() { + let managed_key_id: TariKeyId = TariKeyId::Managed { + branch: random_string(8), + index: { + let mut rng = rand::thread_rng(); + let random_value: u64 = rng.gen(); + random_value + }, + }; + let imported_key_id: TariKeyId = TariKeyId::Imported { + key: PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)), + }; + let zero_key_id: TariKeyId = TariKeyId::Zero; + + let managed_key_id_str = managed_key_id.to_string(); + let imported_key_id_str = imported_key_id.to_string(); + let zero_key_id_str = zero_key_id.to_string(); + + assert_eq!(managed_key_id, TariKeyId::from_str(&managed_key_id_str).unwrap()); + println!("imported_key_id_str: {}", imported_key_id_str); + assert_eq!(imported_key_id, TariKeyId::from_str(&imported_key_id_str).unwrap()); + assert_eq!(zero_key_id, TariKeyId::from_str(&zero_key_id_str).unwrap()); + } +} diff --git a/base_layer/core/src/transactions/key_manager/wrapper.rs b/base_layer/core/src/transactions/key_manager/wrapper.rs index dfd6167837..d1a74a9cb5 100644 --- a/base_layer/core/src/transactions/key_manager/wrapper.rs +++ b/base_layer/core/src/transactions/key_manager/wrapper.rs @@ -296,6 +296,23 @@ where TBackend: KeyManagerBackend + 'static .await } + async fn get_script_signature_from_challenge( + &self, + script_key_id: &TariKeyId, + spend_key_id: &TariKeyId, + value: &PrivateKey, + challenge: &[u8; 64], + r_a: &PrivateKey, + r_x: &PrivateKey, + r_y: &PrivateKey, + ) -> Result { + self.transaction_key_manager_inner + .read() + .await + .get_script_signature_from_challenge(script_key_id, spend_key_id, value, challenge, r_a, r_x, r_y) + .await + } + async fn get_partial_txo_kernel_signature( &self, spend_key_id: &TariKeyId, @@ -420,6 +437,27 @@ where TBackend: KeyManagerBackend + 'static .await } + async fn sign_message(&self, private_key_id: &TariKeyId, challenge: &[u8]) -> Result { + self.transaction_key_manager_inner + .read() + .await + .sign_message(private_key_id, challenge) + .await + } + + async fn sign_with_nonce_and_message( + &self, + private_key_id: &TariKeyId, + nonce: &PrivateKey, + challenge: &[u8], + ) -> Result { + self.transaction_key_manager_inner + .read() + .await + .sign_with_nonce_and_message(private_key_id, nonce, challenge) + .await + } + async fn get_receiver_partial_metadata_signature( &self, spend_key_id: &TariKeyId, @@ -480,6 +518,17 @@ where TBackend: KeyManagerBackend + 'static .generate_burn_proof(spending_key, amount, claim_public_key) .await } + + async fn create_key_pair + Send>( + &self, + branch: T, + ) -> Result<(TariKeyId, PublicKey), KeyManagerServiceError> { + self.transaction_key_manager_inner + .write() + .await + .create_key_pair(&branch.into()) + .await + } } #[async_trait::async_trait] diff --git a/base_layer/core/src/transactions/transaction_components/error.rs b/base_layer/core/src/transactions/transaction_components/error.rs index 2647ea713f..28e0bcaed4 100644 --- a/base_layer/core/src/transactions/transaction_components/error.rs +++ b/base_layer/core/src/transactions/transaction_components/error.rs @@ -82,6 +82,8 @@ pub enum TransactionError { LedgerNotSupported, #[error("Transaction has a zero weight, not possible")] ZeroWeight, + #[error("Output with commitment {0} not found in transaction body")] + OutputNotFound(String), } impl From for TransactionError { diff --git a/base_layer/core/src/transactions/transaction_components/range_proof_type.rs b/base_layer/core/src/transactions/transaction_components/range_proof_type.rs index 1d9e88d9ad..50fb8b1195 100644 --- a/base_layer/core/src/transactions/transaction_components/range_proof_type.rs +++ b/base_layer/core/src/transactions/transaction_components/range_proof_type.rs @@ -23,7 +23,10 @@ // Portions of this file were originally copyrighted (c) 2018 The Grin Developers, issued under the Apache License, // Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0. -use std::fmt::{Display, Formatter}; +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; use borsh::{BorshDeserialize, BorshSerialize}; use num_derive::FromPrimitive; @@ -74,6 +77,18 @@ impl Display for RangeProofType { } } +impl FromStr for RangeProofType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "bullet_proof_plus" => Ok(RangeProofType::BulletProofPlus), + "revealed_value" => Ok(RangeProofType::RevealedValue), + _ => Err("Invalid range proof type".to_string()), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/base_layer/core/src/transactions/transaction_components/wallet_output.rs b/base_layer/core/src/transactions/transaction_components/wallet_output.rs index dcd6f259ad..4b6f8aa8df 100644 --- a/base_layer/core/src/transactions/transaction_components/wallet_output.rs +++ b/base_layer/core/src/transactions/transaction_components/wallet_output.rs @@ -28,8 +28,22 @@ use std::{ fmt::{Debug, Formatter}, }; +use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; -use tari_common_types::types::{ComAndPubSignature, Commitment, FixedHash, PublicKey, RangeProof}; +use tari_common_types::types::{ + ComAndPubSignature, + Commitment, + CommitmentFactory, + FixedHash, + PrivateKey, + PublicKey, + RangeProof, + Signature, +}; +use tari_crypto::{ + commitment::HomomorphicCommitmentFactory, + keys::{PublicKey as PK, SecretKey}, +}; use tari_script::{ExecutionStack, TariScript}; use super::TransactionOutputVersion; @@ -239,6 +253,91 @@ impl WalletOutput { )) } + /// It creates a transaction input given an updated multi-party script signature. The inputs + /// `script_signature_shares` and `script_public_key_shares` exclude the caller's data. + pub async fn to_transaction_input_with_multi_party_script_signature( + &self, + factory: &CommitmentFactory, + script_signature_shares: &[Signature], + script_public_key_shares: &[PublicKey], + key_manager: &KM, + ) -> Result<(TransactionInput, PublicKey), TransactionError> { + let value = self.value.into(); + let commitment = key_manager.get_commitment(&self.spending_key_id, &value).await?; + let version = TransactionInputVersion::get_current_version(); + + let r_a = PrivateKey::random(&mut OsRng); + let r_x = PrivateKey::random(&mut OsRng); + let ephemeral_commitment = factory.commit(&r_x, &r_a); + + let r_y = PrivateKey::random(&mut OsRng); + let ephemeral_public_key_self = PublicKey::from_secret_key(&r_y); + let ephemeral_public_key = script_signature_shares + .iter() + .fold(ephemeral_public_key_self, |acc, x| acc + x.get_public_nonce()); + + let script_public_key_self = key_manager.get_public_key_at_key_id(&self.script_key_id).await?; + let script_public_key = script_public_key_shares + .iter() + .fold(script_public_key_self, |acc, x| acc + x); + + let challenge = TransactionInput::build_script_signature_challenge( + &version, + &ephemeral_commitment, + &ephemeral_public_key, + &self.script, + &self.input_data, + &script_public_key, + &commitment, + ); + let script_signature = key_manager + .get_script_signature_from_challenge( + &self.script_key_id, + &self.spending_key_id, + &value, + &challenge, + &r_a, + &r_x, + &r_y, + ) + .await?; + let multi_party_script_signature = ComAndPubSignature::new( + script_signature.ephemeral_commitment().clone(), + script_signature_shares + .iter() + .fold(script_signature.ephemeral_pubkey().clone(), |acc, x| { + acc + x.get_public_nonce() + }), + script_signature.u_a().clone(), + script_signature.u_x().clone(), + script_signature_shares + .iter() + .fold(script_signature.u_y().clone(), |acc, x| acc + x.get_signature()), + ); + + let input = TransactionInput::new_current_version( + SpentOutput::OutputData { + features: self.features.clone(), + commitment, + script: self.script.clone(), + sender_offset_public_key: self.sender_offset_public_key.clone(), + covenant: self.covenant.clone(), + encrypted_data: self.encrypted_data.clone(), + metadata_signature: self.metadata_signature.clone(), + version: self.version, + minimum_value_promise: self.minimum_value_promise, + rangeproof_hash: match &self.range_proof { + Some(rp) => rp.hash(), + None => FixedHash::zero(), + }, + }, + self.input_data.clone(), + multi_party_script_signature, + ); + + Ok((input, script_public_key)) + } + /// Commits an WalletOutput into a TransactionInput that only contains the hash of the spent output data pub async fn to_compact_transaction_input( &self, diff --git a/base_layer/core/src/transactions/transaction_components/wallet_output_builder.rs b/base_layer/core/src/transactions/transaction_components/wallet_output_builder.rs index 3d90bd1694..b8530684db 100644 --- a/base_layer/core/src/transactions/transaction_components/wallet_output_builder.rs +++ b/base_layer/core/src/transactions/transaction_components/wallet_output_builder.rs @@ -27,7 +27,7 @@ use tari_script::{ExecutionStack, TariScript}; use crate::{ covenants::Covenant, transactions::{ - key_manager::{TariKeyId, TransactionKeyManagerInterface}, + key_manager::{TariKeyId, TransactionKeyManagerBranch, TransactionKeyManagerInterface}, tari_amount::MicroMinotari, transaction_components::{ encrypted_data::PaymentId, @@ -49,6 +49,7 @@ pub struct WalletOutputBuilder { spending_key_id: TariKeyId, features: OutputFeatures, script: Option, + script_lock_height: u64, covenant: Covenant, input_data: Option, script_key_id: Option, @@ -71,6 +72,7 @@ impl WalletOutputBuilder { spending_key_id, features: OutputFeatures::default(), script: None, + script_lock_height: 0, covenant: Covenant::default(), input_data: None, script_key_id: None, @@ -100,6 +102,11 @@ impl WalletOutputBuilder { self } + pub fn with_script_lock_height(mut self, height: u64) -> Self { + self.script_lock_height = height; + self + } + pub fn with_input_data(mut self, input_data: ExecutionStack) -> Self { self.input_data = Some(input_data); self @@ -193,6 +200,76 @@ impl WalletOutputBuilder { Ok(self) } + /// Sign a partial multi-party metadata signature as the sender and receiver - `sender_offset_public_key_shares` and + /// `ephemeral_pubkey_shares` from other participants are combined to enable creation of the challenge. + pub async fn sign_partial_as_sender_and_receiver( + mut self, + key_manager: &KM, + sender_offset_key_id: &TariKeyId, + sender_offset_public_key_shares: &[PublicKey], + ephemeral_public_key_shares: &[PublicKey], + ) -> Result { + let script = self + .script + .as_ref() + .ok_or_else(|| TransactionError::BuilderError("Cannot sign metadata without a script".to_string()))?; + let metadata_message = TransactionOutput::metadata_signature_message_from_parts( + &self.version, + script, + &self.features, + &self.covenant, + &self.encrypted_data, + &self.minimum_value_promise, + ); + + let sender_offset_public_key_self = key_manager.get_public_key_at_key_id(sender_offset_key_id).await?; + let aggregate_sender_offset_public_key = sender_offset_public_key_shares + .iter() + .fold(sender_offset_public_key_self, |acc, x| acc + x); + + let (ephemeral_private_nonce_id, ephemeral_pubkey_self) = key_manager + .get_next_key(&TransactionKeyManagerBranch::MetadataEphemeralNonce.get_branch_key()) + .await?; + let aggregate_ephemeral_pubkey = ephemeral_public_key_shares + .iter() + .fold(ephemeral_pubkey_self, |acc, x| acc + x); + + let receiver_partial_metadata_signature = key_manager + .get_receiver_partial_metadata_signature( + &self.spending_key_id, + &self.value.into(), + &aggregate_sender_offset_public_key, + &aggregate_ephemeral_pubkey, + &TransactionOutputVersion::get_current_version(), + &metadata_message, + self.features.range_proof_type, + ) + .await?; + + let commitment = key_manager + .get_commitment(&self.spending_key_id, &self.value.into()) + .await?; + let ephemeral_commitment = receiver_partial_metadata_signature.ephemeral_commitment(); + let sender_partial_metadata_signature_self = key_manager + .get_sender_partial_metadata_signature( + &ephemeral_private_nonce_id, + sender_offset_key_id, + &commitment, + ephemeral_commitment, + &TransactionOutputVersion::get_current_version(), + &metadata_message, + ) + .await?; + + let metadata_signature = &receiver_partial_metadata_signature + &sender_partial_metadata_signature_self; + + self.metadata_signature = Some(metadata_signature); + self.metadata_signed_by_receiver = true; + self.metadata_signed_by_sender = true; + self.sender_offset_public_key = Some(aggregate_sender_offset_public_key); + Ok(self) + } + pub async fn try_build( self, key_manager: &KM, @@ -222,7 +299,7 @@ impl WalletOutputBuilder { .ok_or_else(|| TransactionError::BuilderError("sender_offset_public_key must be set".to_string()))?, self.metadata_signature .ok_or_else(|| TransactionError::BuilderError("metadata_signature must be set".to_string()))?, - 0, + self.script_lock_height, self.covenant, self.encrypted_data, self.minimum_value_promise, @@ -239,7 +316,7 @@ mod test { use tari_key_manager::key_manager_service::KeyManagerInterface; use super::*; - use crate::transactions::key_manager::{create_memory_db_key_manager, TransactionKeyManagerBranch}; + use crate::transactions::key_manager::create_memory_db_key_manager; #[tokio::test] async fn test_try_build() { diff --git a/base_layer/core/src/transactions/transaction_protocol/sender.rs b/base_layer/core/src/transactions/transaction_protocol/sender.rs index 78530fe0d1..3ef7222f7a 100644 --- a/base_layer/core/src/transactions/transaction_protocol/sender.rs +++ b/base_layer/core/src/transactions/transaction_protocol/sender.rs @@ -723,10 +723,8 @@ impl SenderTransactionProtocol { /// transaction was valid or not. If the result is false, the transaction will be in a Failed state. Calling /// finalize while in any other state will result in an error. /// - /// First we validate against internal sanity checks, then try build the transaction, and then - /// formally validate the transaction terms (no inflation, signature matches etc). If any step fails, - /// the transaction protocol moves to Failed state and we are done; you can't rescue the situation. The function - /// returns `Ok(false)` in this instance. + /// First we validate against internal sanity checks, then try build the transaction. If any step fails, + /// the transaction protocol moves to Failed state and we are done; you can't rescue the situation. pub async fn finalize(&mut self, key_manager: &KM) -> Result<(), TPE> { match &self.state { SenderState::Finalizing(info) => { diff --git a/base_layer/wallet/src/output_manager_service/error.rs b/base_layer/wallet/src/output_manager_service/error.rs index c7ae684627..411741340a 100644 --- a/base_layer/wallet/src/output_manager_service/error.rs +++ b/base_layer/wallet/src/output_manager_service/error.rs @@ -150,6 +150,8 @@ pub enum OutputManagerError { RangeProofError(String), #[error("Transaction is over sized: `{0}`")] TooManyInputsToFulfillTransaction(String), + #[error("Std I/O error: {0}")] + StdIoError(#[from] std::io::Error), } impl From for OutputManagerError { diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index 54caae3e11..16fe77468b 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -23,14 +23,24 @@ use std::{fmt, fmt::Formatter, sync::Arc}; use tari_common_types::{ + tari_address::TariAddress, transaction::TxId, - types::{Commitment, FixedHash, HashOutput, PublicKey}, + types::{Commitment, FixedHash, HashOutput, PublicKey, Signature}, }; +use tari_comms::types::CommsDHKE; use tari_core::{ covenants::Covenant, transactions::{ tari_amount::MicroMinotari, - transaction_components::{OutputFeatures, Transaction, TransactionOutput, WalletOutput, WalletOutputBuilder}, + transaction_components::{ + encrypted_data::PaymentId, + OutputFeatures, + RangeProofType, + Transaction, + TransactionOutput, + WalletOutput, + WalletOutputBuilder, + }, transaction_protocol::{sender::TransactionSenderMessage, TransactionMetadata}, ReceiverTransactionProtocol, SenderTransactionProtocol, @@ -59,6 +69,22 @@ pub enum OutputManagerRequest { UpdateOutputMetadataSignature(Box), GetRecipientTransaction(TransactionSenderMessage), ConfirmPendingTransaction(TxId), + EncumberAggregateUtxo { + tx_id: TxId, + fee_per_gram: MicroMinotari, + output_hash: String, + script_input_shares: Vec, + script_public_key_shares: Vec, + script_signature_shares: Vec, + sender_offset_public_key_shares: Vec, + metadata_ephemeral_public_key_shares: Vec, + dh_shared_secret_shares: Vec, + recipient_address: TariAddress, + payment_id: PaymentId, + maturity: u64, + range_proof_type: RangeProofType, + minimum_value_promise: MicroMinotari, + }, PrepareToSendTransaction { tx_id: TxId, amount: MicroMinotari, @@ -141,6 +167,11 @@ impl fmt::Display for OutputManagerRequest { v.metadata_signature.u_y().to_hex(), v.metadata_signature.u_a().to_hex(), ), + EncumberAggregateUtxo { tx_id, output_hash, .. } => write!( + f, + "Encumber aggregate utxo with tx_id: {} and output_hash: {}", + tx_id, output_hash + ), GetRecipientTransaction(_) => write!(f, "GetRecipientTransaction"), ConfirmPendingTransaction(v) => write!(f, "ConfirmPendingTransaction ({})", v), PrepareToSendTransaction { message, .. } => write!(f, "PrepareToSendTransaction ({})", message), @@ -214,6 +245,7 @@ pub enum OutputManagerResponse { ConvertedToTransactionOutput(Box), OutputMetadataSignatureUpdated, RecipientTransactionGenerated(ReceiverTransactionProtocol), + EncumberAggregateUtxo((Transaction, MicroMinotari, MicroMinotari, PublicKey)), OutputConfirmed, PendingTransactionConfirmed, PayToSelfTransaction((MicroMinotari, Transaction)), @@ -717,6 +749,50 @@ impl OutputManagerHandle { } } + pub async fn encumber_aggregate_utxo( + &mut self, + tx_id: TxId, + fee_per_gram: MicroMinotari, + output_hash: String, + script_input_shares: Vec, + script_public_key_shares: Vec, + script_signature_shares: Vec, + sender_offset_public_key_shares: Vec, + metadata_ephemeral_public_key_shares: Vec, + dh_shared_secret_shares: Vec, + recipient_address: TariAddress, + payment_id: PaymentId, + maturity: u64, + range_proof_type: RangeProofType, + minimum_value_promise: MicroMinotari, + ) -> Result<(Transaction, MicroMinotari, MicroMinotari, PublicKey), OutputManagerError> { + match self + .handle + .call(OutputManagerRequest::EncumberAggregateUtxo { + tx_id, + fee_per_gram, + output_hash, + script_input_shares, + script_public_key_shares, + script_signature_shares, + sender_offset_public_key_shares, + metadata_ephemeral_public_key_shares, + dh_shared_secret_shares, + recipient_address, + payment_id, + maturity, + range_proof_type, + minimum_value_promise, + }) + .await?? + { + OutputManagerResponse::EncumberAggregateUtxo((transaction, amount, fee, total_script_key)) => { + Ok((transaction, amount, fee, total_script_key)) + }, + _ => Err(OutputManagerError::UnexpectedApiResponse), + } + } + pub async fn create_pay_to_self_transaction( &mut self, tx_id: TxId, diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index e15ff8c35d..1f2ae0f421 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -30,14 +30,19 @@ use tari_common::configuration::Network; use tari_common_types::{ tari_address::TariAddress, transaction::TxId, - types::{BlockHash, Commitment, HashOutput, PrivateKey, PublicKey}, + types::{BlockHash, Commitment, FixedHash, HashOutput, PrivateKey, PublicKey, Signature}, }; use tari_comms::{types::CommsDHKE, NodeIdentity}; use tari_core::{ borsh::SerializedSize, consensus::ConsensusConstants, covenants::Covenant, - one_sided::{shared_secret_to_output_encryption_key, stealth_address_script_spending_key}, + one_sided::{ + public_key_to_output_encryption_key, + shared_secret_to_output_encryption_key, + shared_secret_to_output_spending_key, + stealth_address_script_spending_key, + }, proto::base_node::FetchMatchingUtxos, transactions::{ fee::Fee, @@ -48,6 +53,7 @@ use tari_core::{ EncryptedData, KernelFeatures, OutputFeatures, + RangeProofType, Transaction, TransactionError, TransactionOutput, @@ -62,7 +68,16 @@ use tari_core::{ }, }; use tari_crypto::keys::SecretKey; -use tari_script::{inputs, script, ExecutionStack, Opcode, TariScript}; +use tari_script::{ + inputs, + push_pubkey_script, + script, + CheckSigSchnorrSignature, + ExecutionStack, + Opcode, + StackItem, + TariScript, +}; use tari_service_framework::reply_channel; use tari_shutdown::ShutdownSignal; use tari_utilities::{hex::Hex, ByteArray}; @@ -229,6 +244,40 @@ where .add_output(Some(tx_id), *uo, spend_priority) .await .map(|_| OutputManagerResponse::OutputAdded), + OutputManagerRequest::EncumberAggregateUtxo { + tx_id, + fee_per_gram, + output_hash, + script_input_shares, + script_public_key_shares, + script_signature_shares, + sender_offset_public_key_shares, + metadata_ephemeral_public_key_shares, + dh_shared_secret_shares, + recipient_address, + payment_id, + maturity, + range_proof_type, + minimum_value_promise, + } => self + .encumber_aggregate_utxo( + tx_id, + fee_per_gram, + output_hash, + script_input_shares, + script_public_key_shares, + script_signature_shares, + sender_offset_public_key_shares, + metadata_ephemeral_public_key_shares, + dh_shared_secret_shares, + recipient_address, + payment_id, + maturity, + range_proof_type, + minimum_value_promise, + ) + .await + .map(OutputManagerResponse::EncumberAggregateUtxo), OutputManagerRequest::AddUnvalidatedOutput((tx_id, uo, spend_priority)) => self .add_unvalidated_output(tx_id, *uo, spend_priority) .await @@ -1125,6 +1174,274 @@ where Ok((tx_id, stp.into_transaction()?)) } + /// Create a partial transaction in order to prepare output + #[allow(clippy::too_many_lines)] + pub async fn encumber_aggregate_utxo( + &mut self, + tx_id: TxId, + fee_per_gram: MicroMinotari, + output_hash: String, + script_input_shares: Vec, + script_public_key_shares: Vec, + script_signature_shares: Vec, + sender_offset_public_key_shares: Vec, + metadata_ephemeral_public_key_shares: Vec, + dh_shared_secret_shares: Vec, + recipient_address: TariAddress, + payment_id: PaymentId, + maturity: u64, + range_proof_type: RangeProofType, + minimum_value_promise: MicroMinotari, + ) -> Result<(Transaction, MicroMinotari, MicroMinotari, PublicKey), 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_outputs_from_node(vec![output_hash]) + .await? + .pop() + .ok_or_else(|| OutputManagerError::ServiceError(format!("Output not found (TxId: {})", tx_id)))?; + + // Retrieve the list of n public keys from the script + let public_keys = + if let [Opcode::CheckMultiSigVerifyAggregatePubKey(_n, _m, keys, _msg)] = output.script.as_slice() { + keys.clone() + } else { + return Err(OutputManagerError::ServiceError(format!( + "Invalid script (TxId: {})", + tx_id + ))); + }; + // Create a deterministic encryption key from the sum of the public keys + let sum_public_keys = public_keys + .iter() + .fold(tari_common_types::types::PublicKey::default(), |acc, x| acc + x); + let encryption_private_key = public_key_to_output_encryption_key(&sum_public_keys)?; + // Decrypt the output secrets and create a new input as WalletOutput (unblinded) + let input = if let Ok((amount, spending_key, payment_id)) = + EncryptedData::decrypt_data(&encryption_private_key, &output.commitment, &output.encrypted_data) + { + if output.verify_mask(&self.resources.factories.range_proof, &spending_key, amount.as_u64())? { + let mut script_signatures = Vec::new(); + for signature in &script_input_shares { + script_signatures.push(StackItem::Signature(CheckSigSchnorrSignature::new( + signature.get_public_nonce().clone(), + signature.get_signature().clone(), + ))); + } + let spending_key_id = self.resources.key_manager.import_key(spending_key).await?; + WalletOutput::new_with_rangeproof( + output.version, + amount, + spending_key_id, + output.features, + output.script, + ExecutionStack::new(script_signatures), + self.resources.wallet_identity.wallet_node_key_id.clone(), // Only of the master wallet + output.sender_offset_public_key, + output.metadata_signature, + 0, + output.covenant, + output.encrypted_data, + output.minimum_value_promise, + output.proof, + payment_id, + ) + } else { + return Err(OutputManagerError::ServiceError(format!( + "Could not verify mask (TxId: {})", + tx_id + ))); + } + } else { + return Err(OutputManagerError::ServiceError(format!( + "Could not decrypt output (TxId: {})", + tx_id + ))); + }; + + // The entire input will be spent to a single recipient with no change + let output_features = OutputFeatures { + maturity, + range_proof_type, + ..Default::default() + }; + let script = script!(PushPubKey(Box::new(recipient_address.public_spend_key().clone()))); + let metadata_byte_size = self + .resources + .consensus_constants + .transaction_weight_params() + .round_up_features_and_scripts_size( + output_features.get_serialized_size()? + + script.get_serialized_size()? + + Covenant::default().get_serialized_size()?, + ); + let fee = self.get_fee_calc(); + let fee = fee.calculate(fee_per_gram, 1, 1, 1, metadata_byte_size); + let amount = input.value - fee; + + // Create sender transaction protocol builder with recipient data and no change + let mut builder = SenderTransactionProtocol::builder( + self.resources.consensus_constants.clone(), + self.resources.key_manager.clone(), + ); + builder + .with_lock_height(0) + .with_fee_per_gram(fee_per_gram) + .with_kernel_features(KernelFeatures::empty()) + .with_prevent_fee_gt_amount(self.resources.config.prevent_fee_gt_amount) + .with_input(input.clone()) + .await? + .with_recipient_data( + push_pubkey_script(recipient_address.public_spend_key()), + output_features, + Covenant::default(), + minimum_value_promise, + amount, + ) + .await? + .with_change_data( + script!(PushPubKey(Box::default())), + ExecutionStack::default(), + TariKeyId::default(), + TariKeyId::default(), + Covenant::default(), + ); + let mut stp = builder + .build() + .await + .map_err(|e| OutputManagerError::BuildError(e.message))?; + + // This call is needed to advance the state from `SingleRoundMessageReady` to `SingleRoundMessageReady`, + // but the returned value is not used + let _single_round_sender_data = stp.build_single_round_message(&self.resources.key_manager).await?; + + self.confirm_encumberance(tx_id)?; + + // Prepare receiver part of the transaction + + // Diffie-Hellman shared secret `k_Ob * K_Sb = K_Ob * k_Sb` results in a public key, which is fed into + // KDFs to produce the spending and encryption keys. All player's shares are added together to produce the + // shared secret. + let sender_offset_private_key_id_self = + stp.get_recipient_sender_offset_private_key()? + .ok_or(OutputManagerError::ServiceError(format!( + "Missing sender offset private key ID (TxId: {})", + tx_id + )))?; + + let shared_secret = { + let mut key_sum = PublicKey::default(); + for key in &dh_shared_secret_shares { + key_sum = key_sum + &PublicKey::from_vec(&key.as_bytes().to_vec())?; + } + let shared_secret_self = self + .resources + .key_manager + .get_diffie_hellman_shared_secret( + &sender_offset_private_key_id_self, + recipient_address + .public_view_key() + .ok_or(OutputManagerError::ServiceError(format!( + "Missing public view key (TxId: {})", + tx_id + )))?, + ) + .await?; + key_sum = key_sum + &PublicKey::from_vec(&shared_secret_self.as_bytes().to_vec())?; + CommsDHKE::new(&PrivateKey::default(), &key_sum) + }; + + let spending_key = shared_secret_to_output_spending_key(&shared_secret)?; + let spending_key_id = self.resources.key_manager.import_key(spending_key).await?; + + let encryption_private_key = shared_secret_to_output_encryption_key(&shared_secret)?; + let encryption_key_id = self.resources.key_manager.import_key(encryption_private_key).await?; + + let sender_offset_public_key_self = self + .resources + .key_manager + .get_public_key_at_key_id(&sender_offset_private_key_id_self) + .await?; + let sender_offset_public_key = sender_offset_public_key_shares + .iter() + .fold(sender_offset_public_key_self, |acc, x| acc + x); + + let sender_message = TransactionSenderMessage::new_single_round_message( + stp.get_single_round_message(&self.resources.key_manager) + .await + .map_err(|e| service_error_with_id(tx_id, e.to_string(), true))?, + ); + + // Create the output with a partially signed metadata signature + let output = WalletOutputBuilder::new(amount, spending_key_id) + .with_features( + sender_message + .single() + .ok_or( + OutputManagerError::InvalidSenderMessage)? + .features + .clone(), + ) + .with_script(script) + .encrypt_data_for_recovery( + &self.resources.key_manager, + Some(&encryption_key_id), + payment_id.clone(), + ) + .await? + .with_input_data(ExecutionStack::default()) // Just a placeholder in the wallet + .with_sender_offset_public_key(sender_offset_public_key) + .with_script_key(self.resources.wallet_identity.wallet_node_key_id.clone()) + .with_minimum_value_promise(minimum_value_promise) + .sign_partial_as_sender_and_receiver( + &self.resources.key_manager, + &sender_offset_private_key_id_self, + &sender_offset_public_key_shares, + &metadata_ephemeral_public_key_shares, + ) + .await + .map_err(|e|service_error_with_id(tx_id, e.to_string(), true))? + .try_build(&self.resources.key_manager) + .await + .map_err(|e|service_error_with_id(tx_id, e.to_string(), true))?; + + // Finalize the partial transaction - it will not be valid at this stage as the metadata and script + // signatures are not yet complete. + let rtp = ReceiverTransactionProtocol::new( + sender_message, + output, + &self.resources.key_manager, + &self.resources.consensus_constants.clone(), + ) + .await; + let recipient_reply = rtp.get_signed_data()?.clone(); + stp.add_presigned_recipient_info(recipient_reply)?; + stp.finalize(&self.resources.key_manager) + .await + .map_err(|e| service_error_with_id(tx_id, e.to_string(), true))?; + info!(target: LOG_TARGET, "Finalized partial one-side transaction TxId: {}", tx_id); + + // Update the input's script signature + let (updated_input, total_script_public_key) = input + .to_transaction_input_with_multi_party_script_signature( + &self.resources.factories.commitment, + &script_signature_shares, + &script_public_key_shares, + &self.resources.key_manager, + ) + .await?; + + let mut tx = stp.get_transaction()?.clone(); + let mut tx_body = tx.body; + tx_body.update_script_signature(updated_input.commitment()?, &updated_input.script_signature.clone())?; + tx.body = tx_body; + + let fee = stp.get_fee_amount()?; + + Ok((tx, amount, fee, total_script_public_key)) + } + async fn create_pay_to_self_transaction( &mut self, tx_id: TxId, @@ -2461,6 +2778,14 @@ where } } +fn service_error_with_id(tx_id: TxId, err: String, log_error: bool) -> OutputManagerError { + let err_str = format!("TxId: {} ({})", tx_id, err); + if log_error { + error!(target: LOG_TARGET, "{}", err_str); + } + OutputManagerError::ServiceError(err_str) +} + /// This struct holds the detailed balance of the Output Manager Service. #[derive(Debug, Clone, PartialEq)] pub struct Balance { diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 589c1ea6cf..a017a72213 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::{PublicKey, Signature}, + types::{FixedHash, PrivateKey, PublicKey, Signature}, }; use tari_comms::types::CommsPublicKey; use tari_core::{ @@ -46,6 +46,7 @@ use tari_core::{ BuildInfo, CodeTemplateRegistration, OutputFeatures, + RangeProofType, TemplateType, Transaction, TransactionOutput, @@ -53,6 +54,7 @@ use tari_core::{ }, }; use tari_service_framework::reply_channel::SenderService; +use tari_utilities::hex::Hex; use tokio::sync::broadcast; use tower::Service; @@ -99,6 +101,36 @@ pub enum TransactionServiceRequest { message: String, claim_public_key: Option, }, + CreateNMUtxo { + amount: MicroMinotari, + fee_per_gram: MicroMinotari, + n: u8, + m: u8, + public_keys: Vec, + message: [u8; 32], + maturity: u64, + }, + EncumberAggregateUtxo { + fee_per_gram: MicroMinotari, + output_hash: String, + script_input_shares: Vec, + script_public_key_shares: Vec, + script_signature_shares: Vec, + sender_offset_public_key_shares: Vec, + metadata_ephemeral_public_key_shares: Vec, + dh_shared_secret_shares: Vec, + recipient_address: TariAddress, + payment_id: PaymentId, + maturity: u64, + range_proof_type: RangeProofType, + minimum_value_promise: MicroMinotari, + }, + FinalizeSentAggregateTransaction { + tx_id: u64, + total_meta_data_signature: Signature, + total_script_data_signature: Signature, + script_offset: PrivateKey, + }, RegisterValidatorNode { amount: MicroMinotari, validator_node_public_key: CommsPublicKey, @@ -165,6 +197,7 @@ pub enum TransactionServiceRequest { } impl fmt::Display for TransactionServiceRequest { + #[allow(clippy::too_many_lines)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::GetPendingInboundTransactions => write!(f, "GetPendingInboundTransactions"), @@ -186,6 +219,93 @@ impl fmt::Display for TransactionServiceRequest { amount, destination, message ), Self::BurnTari { amount, message, .. } => write!(f, "Burning Tari ({}, {})", amount, message), + Self::CreateNMUtxo { + amount, + fee_per_gram: _, + n, + m, + public_keys: _, + message: _, + maturity: _, + } => f.write_str(&format!( + "Creating a new n-of-m aggregate uxto with: amount = {}, n = {}, m = {}", + amount, n, m + )), + Self::EncumberAggregateUtxo { + fee_per_gram, + output_hash, + script_input_shares, + script_public_key_shares, + script_signature_shares, + sender_offset_public_key_shares, + metadata_ephemeral_public_key_shares, + dh_shared_secret_shares, + recipient_address, + payment_id, + maturity, + range_proof_type, + minimum_value_promise, + .. + } => f.write_str(&format!( + "Creating encumber n-of-m utxo with: fee_per_gram = {}, output_hash = {}, script_input_shares = {:?}, \ + script_public_key_shares = {:?}, script_signature_shares = {:?}, sender_offset_public_key_shares = \ + {:?}, metadata_ephemeral_public_key_shares = {:?}, dh_shared_secret_shares = {:?}, recipient_address \ + = {}, payment_id = {}, maturity = {}, range_proof_type = {}, minimum_value_promise = {}", + fee_per_gram, + output_hash, + script_input_shares + .iter() + .map(|v| format!( + "(sig: {}, nonce: {})", + v.get_signature().to_hex(), + v.get_public_nonce().to_hex() + )) + .collect::>(), + script_public_key_shares + .iter() + .map(|v| v.to_hex()) + .collect::>(), + script_signature_shares + .iter() + .map(|v| format!( + "(sig: {}, nonce: {})", + v.get_signature().to_hex(), + v.get_public_nonce().to_hex() + )) + .collect::>(), + sender_offset_public_key_shares + .iter() + .map(|v| v.to_hex()) + .collect::>(), + metadata_ephemeral_public_key_shares + .iter() + .map(|v| v.to_hex()) + .collect::>(), + dh_shared_secret_shares + .iter() + .map(|v| v.to_hex()) + .collect::>(), + recipient_address, + payment_id, + maturity, + range_proof_type, + minimum_value_promise, + )), + Self::FinalizeSentAggregateTransaction { + tx_id, + total_meta_data_signature, + total_script_data_signature, + script_offset, + } => f.write_str(&format!( + "Finalizing encumbered n-of-m tx(#{}) with: meta_sig(sig: {}, nonce: {}), script_sig(sig: {}, nonce: \ + {}) and script_offset: {}", + tx_id, + total_meta_data_signature.get_signature().to_hex(), + total_meta_data_signature.get_public_nonce().to_hex(), + total_script_data_signature.get_signature().to_hex(), + total_script_data_signature.get_public_nonce().to_hex(), + script_offset.to_hex(), + )), Self::RegisterValidatorNode { validator_node_public_key, message, @@ -254,6 +374,8 @@ impl fmt::Display for TransactionServiceRequest { #[derive(Debug)] pub enum TransactionServiceResponse { TransactionSent(TxId), + TransactionSentWithOutputHash(TxId, FixedHash), + EncumberAggregateUtxo(TxId, Box, Box), TransactionImported(TxId), BurntTransactionSent { tx_id: TxId, @@ -594,6 +716,98 @@ impl TransactionServiceHandle { } } + pub async fn create_aggregate_signature_utxo( + &mut self, + amount: MicroMinotari, + fee_per_gram: MicroMinotari, + n: u8, + m: u8, + public_keys: Vec, + message: [u8; 32], + maturity: u64, + ) -> Result<(TxId, FixedHash), TransactionServiceError> { + match self + .handle + .call(TransactionServiceRequest::CreateNMUtxo { + amount, + fee_per_gram, + n, + m, + public_keys, + message, + maturity, + }) + .await?? + { + TransactionServiceResponse::TransactionSentWithOutputHash(tx_id, output_hash) => Ok((tx_id, output_hash)), + _ => Err(TransactionServiceError::UnexpectedApiResponse), + } + } + + pub async fn encumber_aggregate_utxo( + &mut self, + fee_per_gram: MicroMinotari, + output_hash: String, + script_input_shares: Vec, + script_public_key_shares: Vec, + script_signature_shares: Vec, + sender_offset_public_key_shares: Vec, + metadata_ephemeral_public_key_shares: Vec, + dh_shared_secret_shares: Vec, + recipient_address: TariAddress, + payment_id: PaymentId, + maturity: u64, + range_proof_type: RangeProofType, + minimum_value_promise: MicroMinotari, + ) -> Result<(TxId, Transaction, PublicKey), TransactionServiceError> { + match self + .handle + .call(TransactionServiceRequest::EncumberAggregateUtxo { + fee_per_gram, + output_hash, + script_input_shares, + script_public_key_shares, + script_signature_shares, + sender_offset_public_key_shares, + metadata_ephemeral_public_key_shares, + dh_shared_secret_shares, + recipient_address, + payment_id, + maturity, + range_proof_type, + minimum_value_promise, + }) + .await?? + { + TransactionServiceResponse::EncumberAggregateUtxo(tx_id, transaction, total_script_key) => { + Ok((tx_id, *transaction, *total_script_key)) + }, + _ => Err(TransactionServiceError::UnexpectedApiResponse), + } + } + + pub async fn finalize_aggregate_utxo( + &mut self, + tx_id: u64, + total_meta_data_signature: Signature, + total_script_data_signature: Signature, + script_offset: PrivateKey, + ) -> Result { + match self + .handle + .call(TransactionServiceRequest::FinalizeSentAggregateTransaction { + tx_id, + total_meta_data_signature, + total_script_data_signature, + script_offset, + }) + .await?? + { + TransactionServiceResponse::TransactionSent(tx_id) => Ok(tx_id), + _ => Err(TransactionServiceError::UnexpectedApiResponse), + } + } + pub async fn send_one_sided_to_stealth_address_transaction( &mut self, destination: TariAddress, diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index c72949423d..110876c11b 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -38,28 +38,33 @@ use tari_common_types::{ burnt_proof::BurntProof, tari_address::{TariAddress, TariAddressFeatures}, transaction::{ImportStatus, TransactionDirection, TransactionStatus, TxId}, - types::{PrivateKey, PublicKey, Signature}, + types::{ComAndPubSignature, FixedHash, PrivateKey, PublicKey, Signature}, +}; +use tari_comms::{ + types::{CommsDHKE, CommsPublicKey}, + NodeIdentity, }; -use tari_comms::{types::CommsPublicKey, NodeIdentity}; use tari_comms_dht::outbound::OutboundMessageRequester; use tari_core::{ consensus::ConsensusManager, covenants::Covenant, mempool::FeePerGramStat, one_sided::{ + public_key_to_output_encryption_key, shared_secret_to_output_encryption_key, shared_secret_to_output_spending_key, stealth_address_script_spending_key, }, proto::base_node as base_node_proto, transactions::{ - key_manager::TransactionKeyManagerInterface, + key_manager::{TariKeyId, TransactionKeyManagerInterface}, tari_amount::MicroMinotari, transaction_components::{ encrypted_data::PaymentId, CodeTemplateRegistration, KernelFeatures, OutputFeatures, + RangeProofType, Transaction, TransactionOutput, WalletOutputBuilder, @@ -80,7 +85,7 @@ use tari_crypto::{ }; use tari_key_manager::key_manager_service::KeyId; use tari_p2p::domain_message::DomainMessage; -use tari_script::{inputs, push_pubkey_script, script, ExecutionStack, TariScript}; +use tari_script::{inputs, push_pubkey_script, script, slice_to_boxed_message, ExecutionStack, TariScript}; use tari_service_framework::{reply_channel, reply_channel::Receiver}; use tari_shutdown::ShutdownSignal; use tokio::{ @@ -684,6 +689,85 @@ where tx_id, proof: Box::new(proof), }), + TransactionServiceRequest::CreateNMUtxo { + amount, + fee_per_gram, + n, + m, + public_keys, + message, + maturity, + } => self + .create_aggregate_signature_utxo( + amount, + fee_per_gram, + n, + m, + public_keys, + message, + maturity, + transaction_broadcast_join_handles, + ) + .await + .map(|(tx_id, output_hash)| { + TransactionServiceResponse::TransactionSentWithOutputHash(tx_id, output_hash) + }), + TransactionServiceRequest::EncumberAggregateUtxo { + fee_per_gram, + output_hash, + script_input_shares, + script_public_key_shares, + script_signature_shares, + sender_offset_public_key_shares, + metadata_ephemeral_public_key_shares, + dh_shared_secret_shares, + recipient_address, + payment_id, + maturity, + range_proof_type, + minimum_value_promise, + } => self + .encumber_aggregate_tx( + fee_per_gram, + output_hash, + script_input_shares, + script_public_key_shares, + script_signature_shares, + sender_offset_public_key_shares, + metadata_ephemeral_public_key_shares, + dh_shared_secret_shares + .iter() + .map(|v| CommsDHKE::new(&PrivateKey::default(), &v.clone())) + .collect(), + recipient_address, + payment_id, + maturity, + range_proof_type, + minimum_value_promise, + ) + .await + .map(|(tx_id, tx, total_script_pubkey)| { + TransactionServiceResponse::EncumberAggregateUtxo( + tx_id, + Box::new(tx), + Box::new(total_script_pubkey), + ) + }), + TransactionServiceRequest::FinalizeSentAggregateTransaction { + tx_id, + total_meta_data_signature, + total_script_data_signature, + script_offset, + } => Ok(TransactionServiceResponse::TransactionSent( + self.finalized_aggregate_encumbed_tx( + tx_id.into(), + total_meta_data_signature, + total_script_data_signature, + script_offset, + transaction_broadcast_join_handles, + ) + .await?, + )), TransactionServiceRequest::RegisterValidatorNode { amount, validator_node_public_key, @@ -1085,6 +1169,363 @@ where Ok(()) } + /// Creates a utxo with aggregate public key out of m-of-n public keys + #[allow(clippy::too_many_lines)] + pub async fn create_aggregate_signature_utxo( + &mut self, + amount: MicroMinotari, + fee_per_gram: MicroMinotari, + n: u8, + m: u8, + public_keys: Vec, + message: [u8; 32], + maturity: u64, + transaction_broadcast_join_handles: &mut FuturesUnordered< + JoinHandle>>, + >, + ) -> Result<(TxId, FixedHash), TransactionServiceError> { + let tx_id = TxId::new_random(); + + let msg = slice_to_boxed_message(message.as_bytes()); + let script = script!(CheckMultiSigVerifyAggregatePubKey(n, m, public_keys.clone(), msg)); + + // Empty covenant + let covenant = Covenant::default(); + + // Default range proof + let minimum_value_promise = amount; + + // Prepare sender part of transaction + let mut stp = self + .resources + .output_manager_service + .prepare_transaction_to_send( + tx_id, + amount, + UtxoSelectionCriteria::default(), + OutputFeatures { + range_proof_type: RangeProofType::RevealedValue, + maturity, + ..Default::default() + }, + fee_per_gram, + TransactionMetadata::default(), + "".to_string(), + script.clone(), + covenant.clone(), + minimum_value_promise, + ) + .await?; + let sender_message = TransactionSenderMessage::new_single_round_message( + stp.get_single_round_message(&self.resources.transaction_key_manager_service) + .await?, + ); + + // This call is needed to advance the state from `SingleRoundMessageReady` to `CollectingSingleSignature`, + // but the returned value is not used + let _single_round_sender_data = stp + .build_single_round_message(&self.resources.transaction_key_manager_service) + .await + .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; + + self.resources + .output_manager_service + .confirm_pending_transaction(tx_id) + .await + .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; + + // Prepare receiver part of the transaction + + // In generating an aggregate public key utxo, we can use a randomly generated spend key + let spending_key = PrivateKey::random(&mut OsRng); + let sum_keys = public_keys.iter().fold(PublicKey::default(), |acc, x| acc + x); + let encryption_private_key = public_key_to_output_encryption_key(&sum_keys)?; + + let sender_offset_private_key = stp + .get_recipient_sender_offset_private_key() + .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))? + .ok_or(TransactionServiceProtocolError::new( + tx_id, + TransactionServiceError::InvalidKeyId("Missing sender offset keyid".to_string()), + ))?; + + let encryption_key_id = self + .resources + .transaction_key_manager_service + .import_key(encryption_private_key) + .await?; + + let sender_offset_public_key = self + .resources + .transaction_key_manager_service + .get_public_key_at_key_id(&sender_offset_private_key) + .await?; + + let spending_key_id = self + .resources + .transaction_key_manager_service + .import_key(spending_key.clone()) + .await?; + + let wallet_output = WalletOutputBuilder::new(amount, spending_key_id) + .with_features( + sender_message + .single() + .ok_or(TransactionServiceProtocolError::new( + tx_id, + TransactionServiceError::InvalidMessageError("Sent invalid message type".to_string()), + ))? + .features + .clone(), + ) + .with_script(script) + // We don't want the given utxo to be spendable as an input to a later transaction, so we set + // spendable height of the current utxo to be u64::MAx + .with_script_lock_height(u64::MAX) + .encrypt_data_for_recovery( + &self.resources.transaction_key_manager_service, + Some(&encryption_key_id), + PaymentId::Empty, + ) + .await? + .with_input_data( + ExecutionStack::default(), + ) + .with_covenant(covenant) + .with_sender_offset_public_key(sender_offset_public_key) + .with_script_key(TariKeyId::default()) + .with_minimum_value_promise(minimum_value_promise) + .sign_as_sender_and_receiver( + &self.resources.transaction_key_manager_service, + &sender_offset_private_key, + ) + .await + .unwrap() + .try_build(&self.resources.transaction_key_manager_service) + .await + .unwrap(); + + let tip_height = self.last_seen_tip_height.unwrap_or(0); + let consensus_constants = self.consensus_manager.consensus_constants(tip_height); + let rtp = ReceiverTransactionProtocol::new( + sender_message, + wallet_output.clone(), + &self.resources.transaction_key_manager_service, + consensus_constants, + ) + .await; + let recipient_reply = rtp.get_signed_data()?.clone(); + + // Start finalize + stp.add_single_recipient_info(recipient_reply, &self.resources.transaction_key_manager_service) + .await + .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; + + // Finalize: + stp.finalize(&self.resources.transaction_key_manager_service) + .await + .map_err(|e| { + error!( + target: LOG_TARGET, + "Transaction (TxId: {}) could not be finalized. Failure error: {:?}", tx_id, e, + ); + TransactionServiceProtocolError::new(tx_id, e.into()) + })?; + info!( + target: LOG_TARGET, + "Finalized create n of m transaction TxId: {}", tx_id + ); + + // This event being sent is important, but not critical to the protocol being successful. Send only fails if + // there are no subscribers. + let _size = self + .event_publisher + .send(Arc::new(TransactionEvent::TransactionCompletedImmediately(tx_id))); + + // Broadcast create n of m aggregate public key transaction + let tx = stp + .get_transaction() + .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; + let fee = stp + .get_fee_amount() + .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; + self.resources + .output_manager_service + .add_output_with_tx_id(tx_id, wallet_output.clone(), Some(SpendingPriority::Normal)) + .await?; + self.submit_transaction( + transaction_broadcast_join_handles, + CompletedTransaction::new( + tx_id, + self.resources.wallet_identity.address.clone(), + self.resources.wallet_identity.address.clone(), + amount, + fee, + tx.clone(), + TransactionStatus::Completed, + "".to_string(), + Utc::now().naive_utc(), + TransactionDirection::Outbound, + None, + None, + None, + ) + .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?, + ) + .await?; + + // we want to print out the hash of the utxo + let output_hash = wallet_output + .hash(&self.resources.transaction_key_manager_service) + .await?; + Ok((tx_id, output_hash)) + } + + /// Creates an encumbered uninitialized transaction + pub async fn encumber_aggregate_tx( + &mut self, + fee_per_gram: MicroMinotari, + output_hash: String, + script_input_shares: Vec, + script_public_key_shares: Vec, + script_signature_shares: Vec, + sender_offset_public_key_shares: Vec, + metadata_ephemeral_public_key_shares: Vec, + dh_shared_secret_shares: Vec, + recipient_address: TariAddress, + payment_id: PaymentId, + maturity: u64, + range_proof_type: RangeProofType, + minimum_value_promise: MicroMinotari, + ) -> Result<(TxId, Transaction, PublicKey), TransactionServiceError> { + let tx_id = TxId::new_random(); + + match self + .resources + .output_manager_service + .encumber_aggregate_utxo( + tx_id, + fee_per_gram, + output_hash, + script_input_shares, + script_public_key_shares, + script_signature_shares, + sender_offset_public_key_shares, + metadata_ephemeral_public_key_shares, + dh_shared_secret_shares, + recipient_address, + payment_id, + maturity, + range_proof_type, + minimum_value_promise, + ) + .await + { + Ok((transaction, amount, fee, total_script_key)) => { + let completed_tx = CompletedTransaction::new( + tx_id, + self.resources.wallet_identity.address.clone(), + self.resources.wallet_identity.address.clone(), + amount, + fee, + transaction.clone(), + TransactionStatus::Completed, + "claimed n-of-m utxo".to_string(), + Utc::now().naive_utc(), + TransactionDirection::Inbound, + None, + None, + None, + ) + .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; + self.db.insert_completed_transaction(tx_id, completed_tx)?; + Ok((tx_id, transaction, total_script_key)) + }, + Err(_) => Err(TransactionServiceError::UnexpectedApiResponse), + } + } + + /// Creates an encumbered uninitialized transaction + pub async fn finalized_aggregate_encumbed_tx( + &mut self, + tx_id: TxId, + total_meta_data_signature: Signature, + total_script_data_signature: Signature, + script_offset: PrivateKey, + transaction_broadcast_join_handles: &mut FuturesUnordered< + JoinHandle>>, + >, + ) -> Result { + let mut transaction = self.db.get_completed_transaction(tx_id)?; + + transaction.transaction.script_offset = &transaction.transaction.script_offset + &script_offset; + + transaction.transaction.body.update_metadata_signature( + &transaction.transaction.body.outputs()[0].commitment.clone(), + ComAndPubSignature::new( + transaction.transaction.body.outputs()[0] + .metadata_signature + .ephemeral_commitment() + .clone(), + transaction.transaction.body.outputs()[0] + .metadata_signature + .ephemeral_pubkey() + .clone(), + transaction.transaction.body.outputs()[0].metadata_signature.u_a() + + total_meta_data_signature.get_signature(), + transaction.transaction.body.outputs()[0] + .metadata_signature + .u_x() + .clone(), + transaction.transaction.body.outputs()[0] + .metadata_signature + .u_y() + .clone(), + ), + )?; + transaction.transaction.body.update_script_signature( + &transaction.transaction.body.inputs()[0].commitment()?.clone(), + &ComAndPubSignature::new( + transaction.transaction.body.inputs()[0] + .script_signature + .ephemeral_commitment() + .clone(), + transaction.transaction.body.inputs()[0] + .script_signature + .ephemeral_pubkey() + .clone(), + transaction.transaction.body.inputs()[0].script_signature.u_a() + + total_script_data_signature.get_signature(), + transaction.transaction.body.inputs()[0].script_signature.u_x().clone(), + transaction.transaction.body.inputs()[0].script_signature.u_y().clone(), + ), + )?; + self.resources + .output_manager_service + .update_output_metadata_signature(transaction.transaction.body.outputs()[0].clone()) + .await?; + self.db.update_completed_transaction(tx_id, transaction)?; + + self.resources + .output_manager_service + .confirm_pending_transaction(tx_id) + .await?; + + // Notify that the transaction was successfully resolved. + let _size = self + .event_publisher + .send(Arc::new(TransactionEvent::TransactionCompletedImmediately(tx_id))); + self.complete_send_transaction_protocol( + Ok(TransactionSendResult { + tx_id, + transaction_status: TransactionStatus::Completed, + }), + transaction_broadcast_join_handles, + ); + + Ok(tx_id) + } + /// broadcasts a SHA-XTR atomic swap transaction /// # Arguments /// 'dest_pubkey': The Comms pubkey of the recipient node diff --git a/base_layer/wallet/src/transaction_service/storage/database.rs b/base_layer/wallet/src/transaction_service/storage/database.rs index 3a1bd1490d..08e9f16d1b 100644 --- a/base_layer/wallet/src/transaction_service/storage/database.rs +++ b/base_layer/wallet/src/transaction_service/storage/database.rs @@ -83,6 +83,12 @@ pub trait TransactionBackend: Send + Sync + Clone { fn write(&self, op: WriteOperation) -> Result, TransactionStorageError>; /// Check if a transaction exists in any of the collections fn transaction_exists(&self, tx_id: TxId) -> Result; + /// Update a previously completed transaction with new data + fn update_completed_transaction( + &self, + tx_id: TxId, + transaction: CompletedTransaction, + ) -> Result<(), TransactionStorageError>; /// Complete outbound transaction, this operation must delete the `OutboundTransaction` with the provided /// `TxId` and insert the provided `CompletedTransaction` into `CompletedTransactions`. fn complete_outbound_transaction( @@ -439,6 +445,14 @@ where T: TransactionBackend + 'static Ok(*t) } + pub fn update_completed_transaction( + &self, + tx_id: TxId, + transaction: CompletedTransaction, + ) -> Result<(), TransactionStorageError> { + self.db.update_completed_transaction(tx_id, transaction) + } + pub fn get_imported_transactions(&self) -> Result, TransactionStorageError> { let t = self.db.fetch_imported_transactions()?; Ok(t) diff --git a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs index ad0b1f1764..3b11f18c84 100644 --- a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs +++ b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs @@ -459,6 +459,32 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { Ok(result) } + fn update_completed_transaction( + &self, + tx_id: TxId, + transaction: CompletedTransaction, + ) -> Result<(), TransactionStorageError> { + let start = Instant::now(); + let mut conn = self.database_connection.get_pooled_connection()?; + let acquire_lock = start.elapsed(); + let tx = CompletedTransactionSql::find_by_cancelled(tx_id, false, &mut conn)?; + + tx.delete(&mut conn)?; + let cipher = acquire_read_lock!(self.cipher); + let completed_tx = CompletedTransactionSql::try_from(transaction, &cipher)?; + completed_tx.commit(&mut conn)?; + if start.elapsed().as_millis() > 0 { + trace!( + target: LOG_TARGET, + "sqlite profile - update_completed_transaction: lock {} + db_op {} = {} ms", + acquire_lock.as_millis(), + (start.elapsed() - acquire_lock).as_millis(), + start.elapsed().as_millis() + ); + } + Ok(()) + } + fn get_pending_transaction_counterparty_address_by_tx_id( &self, tx_id: TxId, diff --git a/infrastructure/tari_script/src/lib.rs b/infrastructure/tari_script/src/lib.rs index 9e6eb180de..c3371acf9f 100644 --- a/infrastructure/tari_script/src/lib.rs +++ b/infrastructure/tari_script/src/lib.rs @@ -23,7 +23,16 @@ mod serde; mod stack; pub use error::ScriptError; -pub use op_codes::{slice_to_boxed_hash, slice_to_hash, HashValue, Message, Opcode, OpcodeVersion, ScalarValue}; +pub use op_codes::{ + slice_to_boxed_hash, + slice_to_boxed_message, + slice_to_hash, + HashValue, + Message, + Opcode, + OpcodeVersion, + ScalarValue, +}; pub use script::TariScript; pub use script_context::ScriptContext; pub use stack::{ExecutionStack, StackItem};