From 006229cc4842e838a2ed2d8eb67a3a5129e572ba Mon Sep 17 00:00:00 2001 From: Hansie Odendaal Date: Wed, 19 Jun 2024 17:47:08 +0200 Subject: [PATCH] Apply m_of_n_feature branch - PRs #4759, #4776, #4798, #4822 --- .../minotari_app_utilities/src/utilities.rs | 26 +- .../src/automation/commands.rs | 377 ++++++++++++++-- .../src/automation/error.rs | 3 + .../minotari_console_wallet/src/cli.rs | 108 ++++- .../src/wallet_modes.rs | 75 +++- .../comms/src/ledger_wallet.rs | 1 + .../get_script_signature_from_challenge.rs | 79 ++++ .../minotari_ledger_wallet/wallet/src/main.rs | 4 + base_layer/core/src/common/one_sided.rs | 12 +- .../core/src/transactions/aggregated_body.rs | 40 +- .../src/transactions/key_manager/inner.rs | 99 ++++ .../src/transactions/key_manager/interface.rs | 8 + .../src/transactions/key_manager/wrapper.rs | 14 + .../transaction_components/error.rs | 2 + .../transaction_components/wallet_output.rs | 71 ++- .../wallet_output_builder.rs | 9 +- .../transaction_protocol/sender.rs | 6 +- .../src/output_manager_service/handle.rs | 53 ++- .../src/output_manager_service/service.rs | 172 ++++++- .../wallet/src/transaction_service/handle.rs | 156 ++++++- .../wallet/src/transaction_service/service.rs | 421 +++++++++++++++++- .../transaction_service/storage/database.rs | 14 + .../transaction_service/storage/sqlite_db.rs | 26 ++ infrastructure/tari_script/src/lib.rs | 11 +- 24 files changed, 1725 insertions(+), 62 deletions(-) create mode 100644 applications/minotari_ledger_wallet/wallet/src/handlers/get_script_signature_from_challenge.rs diff --git a/applications/minotari_app_utilities/src/utilities.rs b/applications/minotari_app_utilities/src/utilities.rs index 0142b8c93c3..7683de09f26 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}; @@ -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/src/automation/commands.rs b/applications/minotari_console_wallet/src/automation/commands.rs index 6479447d17c..1f4dee6b380 100644 --- a/applications/minotari_console_wallet/src/automation/commands.rs +++ b/applications/minotari_console_wallet/src/automation/commands.rs @@ -46,15 +46,15 @@ use minotari_wallet::{ WalletConfig, WalletSqlite, }; +use rand::rngs::OsRng; 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,18 +62,27 @@ use tari_comms::{ types::CommsPublicKey, }; use tari_comms_dht::{envelope::NodeDestination, DhtDiscoveryRequester}; -use tari_core::transactions::{ - key_manager::TransactionKeyManagerInterface, - 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, + Transaction, + TransactionInput, + TransactionInputVersion, + TransactionOutput, + TransactionOutputVersion, + UnblindedOutput, + WalletOutput, + }, }, }; -use tari_crypto::ristretto::RistrettoSecretKey; +use tari_crypto::{keys::SecretKey, ristretto::RistrettoSecretKey}; +use tari_script::{script, ExecutionStack, TariScript}; use tari_utilities::{hex::Hex, ByteArray}; use tokio::{ sync::{broadcast, mpsc}, @@ -88,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 {} @@ -158,6 +139,74 @@ 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, +) -> 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) + .await + .map_err(CommandError::TransactionServiceError) +} + +/// encumbers a n-of-m transaction +async fn encumber_aggregate_utxo( + mut wallet_transaction_service: TransactionServiceHandle, + fee_per_gram: MicroMinotari, + output_hash: String, + signatures: Vec, + total_script_pubkey: PublicKey, + total_offset_pubkey: PublicKey, + total_signature_nonce: PublicKey, + metadata_signature_nonce: PublicKey, + wallet_script_secret_key: String, +) -> Result<(TxId, Transaction, PublicKey), CommandError> { + wallet_transaction_service + .encumber_aggregate_utxo( + fee_per_gram, + output_hash, + signatures, + total_script_pubkey, + total_offset_pubkey, + total_signature_nonce, + metadata_signature_nonce, + wallet_script_secret_key, + ) + .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, @@ -272,6 +321,18 @@ pub async fn coin_split( Ok(tx_id) } +pub fn sign_message(private_key: String, challenge: String) -> Result { + let private_key = + PrivateKey::from_hex(private_key.as_str()).map_err(|e| CommandError::InvalidArgument(e.to_string()))?; + let challenge = challenge.as_bytes(); + + let nonce = PrivateKey::random(&mut OsRng); + let signature = Signature::sign_with_nonce_and_message(&private_key, nonce, challenge) + .map_err(CommandError::FailedSignature)?; + + Ok(signature) +} + async fn wait_for_comms(connectivity_requester: &ConnectivityRequester) -> Result<(), CommandError> { let mut connectivity = connectivity_requester.get_event_subscription(); print!("Waiting for connectivity... "); @@ -690,6 +751,242 @@ pub async fn command_runner( }, 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, + ) + .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 sign_message(args.private_key, args.challenge) { + 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) => { + let mut total_script_pub_key = PublicKey::default(); + for sig in args.script_pubkeys { + total_script_pub_key = sig.into(); + } + let mut total_offset_pub_key = PublicKey::default(); + for sig in args.offset_pubkeys { + total_offset_pub_key = sig.into(); + } + let mut total_sig_nonce = PublicKey::default(); + for sig in args.script_signature_nonces { + total_sig_nonce = sig.into(); + } + let mut total_meta_nonce = PublicKey::default(); + for sig in args.metadata_signature_nonces { + total_meta_nonce = sig.into(); + } + match encumber_aggregate_utxo( + transaction_service.clone(), + args.fee_per_gram, + args.output_hash, + args.signatures.iter().map(|sgn| sgn.clone().into()).collect::>(), + total_script_pub_key, + total_offset_pub_key, + total_sig_nonce, + total_meta_nonce, + args.wallet_script_secret_key, + ) + .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_key = + PrivateKey::from_hex(&args.secret_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 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, + ); + // TODO: Change to `ComAndPubSignature` + let signature = Signature::sign_with_nonce_and_message(&private_key, private_nonce, challenge) + .map_err(CommandError::FailedSignature)?; + println!( + "Sign script sig: + 1. signature: {}, + 2. public nonce: {}", + signature.get_signature().to_hex(), + signature.get_public_nonce().to_hex(), + ) + }, + 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 395e59992bb..10887d3588a 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 054012a879d..22be8f435ec 100644 --- a/applications/minotari_console_wallet/src/cli.rs +++ b/applications/minotari_console_wallet/src/cli.rs @@ -28,7 +28,10 @@ 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; @@ -117,6 +120,12 @@ pub enum CliCommands { SendMinotari(SendMinotariArgs), BurnMinotari(BurnMinotariArgs), CreateKeyPair(CreateKeyPairArgs), + CreateAggregateSignatureUtxo(CreateAggregateSignatureUtxoArgs), + EncumberAggregateUtxo(EncumberAggregateUtxoArgs), + SpendAggregateUtxo(SpendAggregateUtxoArgs), + SignMessage(SignMessageArgs), + CreateScriptSig(CreateScriptSigArgs), + CreateMetaSig(CreateMetaSigArgs), SendOneSided(SendMinotariArgs), SendOneSidedToStealthAddress(SendMinotariArgs), MakeItRain(MakeItRainArgs), @@ -161,9 +170,104 @@ pub struct BurnMinotariArgs { #[derive(Debug, Args, Clone)] pub struct CreateKeyPairArgs { - #[clap(short, long, default_value = "Burn funds")] + #[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 public_keys: Vec, +} + +#[derive(Debug, Args, Clone)] +pub struct SignMessageArgs { + #[clap(long)] + pub private_key: String, + #[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 wallet_script_secret_key: String, + #[clap(long)] + pub script_pubkeys: Vec, + #[clap(long)] + pub offset_pubkeys: Vec, + #[clap(long)] + pub script_signature_nonces: Vec, + #[clap(long)] + pub metadata_signature_nonces: Vec, + #[clap(long)] + pub signatures: Vec, +} + +#[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 secret_key: String, + #[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 a050a4667ad..4ca75a7e09f 100644 --- a/applications/minotari_console_wallet/src/wallet_modes.rs +++ b/applications/minotari_console_wallet/src/wallet_modes.rs @@ -491,17 +491,68 @@ mod test { discover-peer f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 send-minotari --message Our_secret! 125T \ - 2603fed9cf87097105913096da423ae4e3096e44a172185742ce5bc00d27016cd81118 + 2603fed9cf87097105913096da423ae4e3096e44a172185742ce5bc00d27016cd81118 burn-minotari --message Ups_these_funds_will_be_burned! 100T create-key-pair --key-branch pie + create-aggregate-signature-utxo --amount 125T --m 2 --n 3 --fee-per-gram 1 --message ff \ + --public-keys=5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ + --public-keys=f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 + + encumber-aggregate-utxo \ + --fee-per-gram 1 \ + --output-hash f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ + --wallet-script-secret-key 2603fed9cf87097105913096da423ae4e3096e44a172185742ce5bc00d27016cd81118 \ + --script-pubkeys=5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ + --script-pubkeys=f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ + --offset-pubkeys=5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ + --offset-pubkeys=f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ + --script-signature-nonces=5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ + --script-signature-nonces=f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ + --metadata-signature-nonces=5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ + --metadata-signature-nonces=f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 \ + --signatures=3ddde10d0775c20fb25015546c6a8068812044e7ca4ee1057e84ec9ab6705d03,8a55d1cb503be36875d38f2dc6abac7b23445bbd7253684a1506f5ee1855cd58 \ + --signatures=3edf1ed103b0ac0bbad6a6de8369808d14dfdaaf294fe660646875d749a1f908,50a26c646db951720c919f59cd7a34600a7fc3ee978c64fbcce0ad184c46844c + + 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 + + sign-message \ + --private-key 2603fed9cf87097105913096da423ae4e3096e44a172185742ce5bc00d27016cd81118 \ + --challenge f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 + + create-script-sig \ + --secret-key 2603fed9cf87097105913096da423ae4e3096e44a172185742ce5bc00d27016cd81118 \ + --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 \ - --start-time now --message Stressing_it_a_bit...!_(from_Feeling-a-bit-Generous) \ - 2603fed9cf87097105913096da423ae4e3096e44a172185742ce5bc00d27016cd81118 + --start-time now --message Stressing_it_a_bit...!_(from_Feeling-a-bit-Generous) \ + 2603fed9cf87097105913096da423ae4e3096e44a172185742ce5bc00d27016cd81118 export-tx 123456789 --output-file pie.txt @@ -517,6 +568,12 @@ mod test { 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; @@ -529,6 +586,12 @@ mod test { 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::SendOneSided(_) => {}, CliCommands::SendOneSidedToStealthAddress(_) => {}, CliCommands::MakeItRain(_) => make_it_rain = true, @@ -564,6 +627,12 @@ mod test { 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 297b5617ac9..48548db0561 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 00000000000..268f7f9630b --- /dev/null +++ b/applications/minotari_ledger_wallet/wallet/src/handlers/get_script_signature_from_challenge.rs @@ -0,0 +1,79 @@ +// 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 = derive_from_bip32_key(account, u32::random().into(), KeyType::Nonce)?; + let r_x = derive_from_bip32_key(account, u32::random().into(), KeyType::Nonce)?; + let r_y = derive_from_bip32_key(account, u32::random().into(), KeyType::Nonce)?; + + 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 f8527b02ad9..19e400c55f2 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 34958668ac5..a63907c410f 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,16 @@ 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 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 203b43a3d7a..fbc0fc6fa6e 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; + + 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 952dea90131..a3216c27a33 100644 --- a/base_layer/core/src/transactions/key_manager/inner.rs +++ b/base_layer/core/src/transactions/key_manager/inner.rs @@ -676,6 +676,105 @@ 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], + ) -> 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); + + 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 r_a = PrivateKey::random(&mut OsRng); + let r_x = PrivateKey::random(&mut OsRng); + let r_y = PrivateKey::random(&mut OsRng); + 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) // ----------------------------------------------------------------------------------------------------------------- diff --git a/base_layer/core/src/transactions/key_manager/interface.rs b/base_layer/core/src/transactions/key_manager/interface.rs index 45a9a9e4345..f517a95e009 100644 --- a/base_layer/core/src/transactions/key_manager/interface.rs +++ b/base_layer/core/src/transactions/key_manager/interface.rs @@ -186,6 +186,14 @@ 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], + ) -> Result; + async fn get_partial_txo_kernel_signature( &self, spend_key_id: &TariKeyId, diff --git a/base_layer/core/src/transactions/key_manager/wrapper.rs b/base_layer/core/src/transactions/key_manager/wrapper.rs index a9801bfb1ff..055c40a489a 100644 --- a/base_layer/core/src/transactions/key_manager/wrapper.rs +++ b/base_layer/core/src/transactions/key_manager/wrapper.rs @@ -297,6 +297,20 @@ 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], + ) -> Result { + self.transaction_key_manager_inner + .read() + .await + .get_script_signature_from_challenge(script_key_id, spend_key_id, value, challenge) + .await + } + async fn get_partial_txo_kernel_signature( &self, spend_key_id: &TariKeyId, diff --git a/base_layer/core/src/transactions/transaction_components/error.rs b/base_layer/core/src/transactions/transaction_components/error.rs index 2647ea713f3..28e0bcaed40 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/wallet_output.rs b/base_layer/core/src/transactions/transaction_components/wallet_output.rs index dcd6f259ad8..d98ffb1ba97 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,18 @@ 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, +}; +use tari_crypto::{commitment::HomomorphicCommitmentFactory, keys::SecretKey}; use tari_script::{ExecutionStack, TariScript}; use super::TransactionOutputVersion; @@ -239,6 +249,65 @@ impl WalletOutput { )) } + /// It creates a transaction input given a partial script signature. The public keys + /// `partial_script_public_key` and `partial_total_nonce` exclude caller's private keys + pub async fn as_transaction_input_with_partial_signature( + &self, + factory: &CommitmentFactory, + partial_script_public_key: PublicKey, + partial_total_nonce: PublicKey, + key_manager: &KM, + ) -> Result { + let value = self.value.into(); + let commitment = key_manager.get_commitment(&self.spending_key_id, &value).await?; + + let version = TransactionInputVersion::get_current_version(); + let script_nonce_a = PrivateKey::random(&mut OsRng); + let script_nonce_b = PrivateKey::random(&mut OsRng); + let ephemeral_commitment = factory.commit(&script_nonce_b, &script_nonce_a); + // TODO: Is this correct? It seems `PublicKey::default()` should be repalced with some non-default value + let ephemeral_pubkey = PublicKey::default() + &partial_total_nonce; + let script_public_key = + key_manager.get_public_key_at_key_id(&self.script_key_id).await? + &partial_script_public_key; + let challenge = TransactionInput::build_script_signature_challenge( + &version, + &ephemeral_commitment, + &ephemeral_pubkey, + &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) + .await?; + // TODO: Is this correct? `partial_total_nonce` is already added to `ephemeral_pubkey` above + let script_signature = ComAndPubSignature::new( + script_signature.ephemeral_commitment().clone(), + script_signature.ephemeral_pubkey().clone() + &partial_total_nonce, + script_signature.u_a().clone(), + script_signature.u_x().clone(), + script_signature.u_y().clone(), + ); + + Ok(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: Default::default(), + metadata_signature: Default::default(), + version: self.version, + minimum_value_promise: self.minimum_value_promise, + rangeproof_hash: Default::default(), + }, + self.input_data.clone(), + script_signature, + )) + } + /// 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 3d90bd16942..b4cdfce5b3b 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 @@ -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 @@ -222,7 +229,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, diff --git a/base_layer/core/src/transactions/transaction_protocol/sender.rs b/base_layer/core/src/transactions/transaction_protocol/sender.rs index 99be0d75d7c..0d719f7ea03 100644 --- a/base_layer/core/src/transactions/transaction_protocol/sender.rs +++ b/base_layer/core/src/transactions/transaction_protocol/sender.rs @@ -724,10 +724,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/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index 54caae3e114..b07175d5ece 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -24,7 +24,7 @@ use std::{fmt, fmt::Formatter, sync::Arc}; use tari_common_types::{ transaction::TxId, - types::{Commitment, FixedHash, HashOutput, PublicKey}, + types::{Commitment, FixedHash, HashOutput, PublicKey, Signature}, }; use tari_core::{ covenants::Covenant, @@ -59,6 +59,17 @@ pub enum OutputManagerRequest { UpdateOutputMetadataSignature(Box), GetRecipientTransaction(TransactionSenderMessage), ConfirmPendingTransaction(TxId), + EncumberAggregateUtxo { + tx_id: TxId, + fee_per_gram: MicroMinotari, + output_hash: String, + signatures: Vec, + total_script_pubkey: PublicKey, + total_offset_pubkey: PublicKey, + total_signature_nonce: PublicKey, + metadata_signature_nonce: PublicKey, + wallet_script_secret_key: String, + }, PrepareToSendTransaction { tx_id: TxId, amount: MicroMinotari, @@ -141,6 +152,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 +230,7 @@ pub enum OutputManagerResponse { ConvertedToTransactionOutput(Box), OutputMetadataSignatureUpdated, RecipientTransactionGenerated(ReceiverTransactionProtocol), + EncumberAggregateUtxo((Transaction, MicroMinotari, MicroMinotari, PublicKey)), OutputConfirmed, PendingTransactionConfirmed, PayToSelfTransaction((MicroMinotari, Transaction)), @@ -717,6 +734,40 @@ impl OutputManagerHandle { } } + pub async fn encumber_aggregate_utxo( + &mut self, + tx_id: TxId, + fee_per_gram: MicroMinotari, + output_hash: String, + signatures: Vec, + total_script_pubkey: PublicKey, + total_offset_pubkey: PublicKey, + total_signature_nonce: PublicKey, + metadata_signature_nonce: PublicKey, + wallet_script_secret_key: String, + ) -> Result<(Transaction, MicroMinotari, MicroMinotari, PublicKey), OutputManagerError> { + match self + .handle + .call(OutputManagerRequest::EncumberAggregateUtxo { + tx_id, + fee_per_gram, + output_hash, + signatures, + total_script_pubkey, + total_offset_pubkey, + total_signature_nonce, + metadata_signature_nonce, + wallet_script_secret_key, + }) + .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 48c9f62e755..031d4511c96 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -30,7 +30,7 @@ use tari_common::configuration::Network; use tari_common_types::{ tari_address::TariAddress, transaction::TxId, - types::{BlockHash, Commitment, HashOutput, PrivateKey, PublicKey}, + types::{BlockHash, Commitment, HashOutput, PrivateKey, PublicKey, Signature}, }; use tari_comms::{types::CommsDHKE, NodeIdentity}; use tari_core::{ @@ -229,6 +229,30 @@ where .add_output(Some(tx_id), *uo, spend_priority) .await .map(|_| OutputManagerResponse::OutputAdded), + OutputManagerRequest::EncumberAggregateUtxo { + tx_id, + fee_per_gram, + output_hash, + signatures, + total_script_pubkey, + total_offset_pubkey, + total_signature_nonce, + metadata_signature_nonce, + wallet_script_secret_key, + } => self + .encumber_aggregate_utxo( + tx_id, + fee_per_gram, + output_hash, + signatures, + total_script_pubkey, + total_offset_pubkey, + total_signature_nonce, + metadata_signature_nonce, + wallet_script_secret_key, + ) + .await + .map(OutputManagerResponse::EncumberAggregateUtxo), OutputManagerRequest::AddUnvalidatedOutput((tx_id, uo, spend_priority)) => self .add_unvalidated_output(tx_id, *uo, spend_priority) .await @@ -1125,6 +1149,152 @@ where Ok((tx_id, stp.into_transaction()?)) } + #[allow(clippy::too_many_lines)] + pub async fn encumber_aggregate_utxo( + &mut self, + _tx_id: TxId, + _fee_per_gram: MicroMinotari, + _output_hash: String, + _signatures: Vec, + _total_script_pubkey: PublicKey, + _total_offset_pubkey: PublicKey, + _total_signature_nonce: PublicKey, + _metadata_signature_nonce: PublicKey, + _wallet_script_secret_key: String, + ) -> Result<(Transaction, MicroMinotari, MicroMinotari, PublicKey), OutputManagerError> { + unimplemented!("encumber_aggregate_utxo"); + + // let script = script!(Nop); + // let covenant = Covenant::default(); + // let output_features = OutputFeatures::default(); + // + // 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.get_serialized_size()?, + // ); + // + // let output_hash = + // FixedHash::from_hex(&output_hash).map_err(|e| OutputManagerError::ConversionError(e.to_string()))?; + // let db_input = self.resources.db.get_unspent_output(output_hash)?; + // let mut input: WalletOutput = db_input.clone().into(); + // + // let mut script_signatures = Vec::new(); + // + // for signature in &signatures { + // script_signatures.push(StackItem::Signature(CheckSigSchnorrSignature::new(signature.get_public_nonce(). + // clone(), signature.get_signature().clone()))); } + // input.input_data = ExecutionStack::new(script_signatures); + // + // let wallet_script_secret_key = PrivateKey::from_hex(&wallet_script_secret_key) + // .map_err(|e| OutputManagerError::ConversionError(e.to_string()))?; + // let total_script_key = PublicKey::from_secret_key(&wallet_script_secret_key) + &total_script_pubkey; + // input.script_key_id = self.resources.key_manager.import_key(wallet_script_secret_key).await?; + // + // let offset = PrivateKey::random(&mut OsRng); + // let nonce = PrivateKey::random(&mut OsRng); + // let sender_offset_private_key = PrivateKey::random(&mut OsRng); + // + // // Create builder with no recipients (other than ourselves) + // let mut builder = SenderTransactionProtocol::builder(self.resources.consensus_constants.clone(), + // self.resources.key_manager.clone()); builder + // .with_fee_per_gram(fee_per_gram) + // // .with_offset(offset.clone()) + // // .with_private_nonce(nonce.clone()) + // // .with_rewindable_outputs(self.resources.rewind_data.clone()) + // .with_prevent_fee_gt_amount(self.resources.config.prevent_fee_gt_amount) + // .with_kernel_features(KernelFeatures::empty()) + // .with_tx_id(tx_id) + // .with_lock_height(0) + // // TODO: The input to this function has changed; it needs a `WalletOutput`, not a `TransactionInput` + // .with_input( + // input.clone().as_transaction_input_with_partial_signature( + // &self.resources.factories.commitment, + // total_script_pubkey, + // total_signature_nonce, + // &() + // )?).await?; + // + // + // + // let fee = self.get_fee_calc(); + // let fee = fee.calculate(fee_per_gram, 1, 1, 1, metadata_byte_size); + // let amount = input.value - fee; + // + // let (spending_key, script_private_key) = self.get_spend_and_script_keys().await?; + // let encrypted_value = EncryptedValue::default(); + // let minimum_amount_promise = MicroTari::zero(); + // let metadata_signature = TransactionOutput::create_metadata_signature( + // TransactionOutputVersion::get_current_version(), + // amount, + // &spending_key, + // &script, + // &output_features, + // &(&total_offset_pubkey + &PublicKey::from_secret_key(&sender_offset_private_key)), + // Some(&metadata_signature_nonce), + // Some(&sender_offset_private_key), + // &covenant, + // &encrypted_value, + // minimum_amount_promise, + // )?; + // + // let metadata_signature = ComSignature::new( + // metadata_signature.public_nonce() + &metadata_signature_nonce, + // metadata_signature.u().clone(), + // metadata_signature.v().clone(), + // ); + // let utxo = DbUnblindedOutput::rewindable_from_unblinded_output( + // UnblindedOutput::new_current_version( + // amount, + // spending_key, + // output_features, + // script, + // inputs!(PublicKey::from_secret_key(&script_private_key)), + // script_private_key, + // &total_offset_pubkey + &PublicKey::from_secret_key(&sender_offset_private_key), + // metadata_signature, + // u64::MAX, + // covenant, + // encrypted_value, + // minimum_amount_promise, + // ), + // &self.resources.factories, + // &self.resources.rewind_data, + // None, + // None, + // OutputSource::default(), + // )?; + // builder + // .with_output(utxo.unblinded_output.clone(), sender_offset_private_key.clone()) + // .map_err(|e| OutputManagerError::BuildError(e.message))?; + // let factories = CryptoFactories::default(); + // let mut stp = builder + // .build( + // &self.resources.factories, + // None, + // self.last_seen_tip_height.unwrap_or(u64::MAX), + // ) + // .map_err(|e| OutputManagerError::BuildError(e.message))?; + // + // trace!( + // target: LOG_TARGET, + // "Encumber send to self transaction ({}) outputs.", + // tx_id + // ); + // self.resources.db.encumber_outputs(tx_id, vec![db_input], vec![utxo])?; + // + // trace!(target: LOG_TARGET, "Finalize send-to-self transaction ({}).", tx_id); + // + // stp.finalize(&self.resources.key_manager).await?; + // let tx = stp.take_transaction()?; + // + // Ok((tx, amount, fee, total_script_key)) + } + async fn create_pay_to_self_transaction( &mut self, tx_id: TxId, diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 589c1ea6cf4..f6df1ac2e63 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::{ @@ -53,6 +53,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 +100,30 @@ 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], + }, + EncumberAggregateUtxo { + fee_per_gram: MicroMinotari, + output_hash: String, + signatures: Vec, + total_script_pubkey: PublicKey, + total_offset_pubkey: PublicKey, + total_signature_nonce: PublicKey, + metadata_signature_nonce: PublicKey, + wallet_script_secret_key: String, + }, + 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, @@ -186,6 +211,53 @@ 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: _, + } => 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, + signatures, + total_script_pubkey, + total_offset_pubkey, + total_signature_nonce, + metadata_signature_nonce, + .. + } => f.write_str(&format!( + "Creating encumber n-of-m utxo with: fee_per_gram = {}, output_hash = {}, signatures = {:?}, \ + total_script_pubkey = {}, total_offset_pubkey = {}, total_signature_nonce = {}, \ + metadata_signature_nonce = {}", + fee_per_gram, + output_hash, + signatures, + total_script_pubkey.to_hex(), + total_offset_pubkey.to_hex(), + total_signature_nonce.to_hex(), + metadata_signature_nonce.to_hex(), + )), + 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 +326,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 +668,86 @@ 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], + ) -> Result<(TxId, FixedHash), TransactionServiceError> { + match self + .handle + .call(TransactionServiceRequest::CreateNMUtxo { + amount, + fee_per_gram, + n, + m, + public_keys, + message, + }) + .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, + signatures: Vec, + total_script_pubkey: PublicKey, + total_offset_pubkey: PublicKey, + total_signature_nonce: PublicKey, + metadata_signature_nonce: PublicKey, + wallet_script_secret_key: String, + ) -> Result<(TxId, Transaction, PublicKey), TransactionServiceError> { + match self + .handle + .call(TransactionServiceRequest::EncumberAggregateUtxo { + fee_per_gram, + output_hash, + signatures, + total_script_pubkey, + total_offset_pubkey, + total_signature_nonce, + metadata_signature_nonce, + wallet_script_secret_key, + }) + .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 a1cb3c7fa24..ff5e5285015 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -38,7 +38,7 @@ use tari_common_types::{ burnt_proof::BurntProof, tari_address::{TariAddress, TariAddressFeatures}, transaction::{ImportStatus, TransactionDirection, TransactionStatus, TxId}, - types::{PrivateKey, PublicKey, Signature}, + types::{ComAndPubSignature, FixedHash, PrivateKey, PublicKey, Signature}, }; use tari_comms::{types::CommsPublicKey, NodeIdentity}; use tari_comms_dht::outbound::OutboundMessageRequester; @@ -48,6 +48,7 @@ use tari_core::{ mempool::FeePerGramStat, one_sided::{ diffie_hellman_stealth_domain_hasher, + secret_key_to_output_encryption_key, shared_secret_to_output_encryption_key, shared_secret_to_output_spending_key, stealth_address_script_spending_key, @@ -81,7 +82,15 @@ use tari_crypto::{ }; use tari_key_manager::key_manager_service::KeyId; use tari_p2p::domain_message::DomainMessage; -use tari_script::{inputs, one_sided_payment_script, script, stealth_payment_script, ExecutionStack, TariScript}; +use tari_script::{ + inputs, + one_sided_payment_script, + script, + slice_to_boxed_message, + stealth_payment_script, + ExecutionStack, + TariScript, +}; use tari_service_framework::{reply_channel, reply_channel::Receiver}; use tari_shutdown::ShutdownSignal; use tokio::{ @@ -685,6 +694,70 @@ where tx_id, proof: Box::new(proof), }), + TransactionServiceRequest::CreateNMUtxo { + amount, + fee_per_gram, + n, + m, + public_keys, + message, + } => self + .create_aggregate_signature_utxo( + amount, + fee_per_gram, + n, + m, + public_keys, + message, + transaction_broadcast_join_handles, + ) + .await + .map(|(tx_id, output_hash)| { + TransactionServiceResponse::TransactionSentWithOutputHash(tx_id, output_hash) + }), + TransactionServiceRequest::EncumberAggregateUtxo { + fee_per_gram, + output_hash, + signatures, + total_script_pubkey, + total_offset_pubkey, + total_signature_nonce, + metadata_signature_nonce, + wallet_script_secret_key, + } => self + .encumber_aggregate_tx( + fee_per_gram, + output_hash, + signatures, + total_script_pubkey, + total_offset_pubkey, + total_signature_nonce, + metadata_signature_nonce, + wallet_script_secret_key, + ) + .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, @@ -1086,6 +1159,350 @@ 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], + 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, msg)); + + // Empty covenant + let covenant = Covenant::default(); + + // Default range proof + let minimum_value_promise = MicroMinotari::zero(); + + // Prepare sender part of transaction + let mut stp = self + .resources + .output_manager_service + .prepare_transaction_to_send( + tx_id, + amount, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), + fee_per_gram, + TransactionMetadata::default(), + "".to_string(), + script.clone(), + covenant, + 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 encryption_private_key = secret_key_to_output_encryption_key(&spending_key)?; + + 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 minimum_value_promise = MicroMinotari::zero(); + let covenant = Covenant::default(); + 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( + // TODO: refactor this, when we have implemented the necessary logic + inputs!(PublicKey::from_secret_key(&spending_key)), + ) + .with_covenant(covenant) + .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_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, + signatures: Vec, + total_script_pubkey: PublicKey, + total_offset_pubkey: PublicKey, + total_signature_nonce: PublicKey, + metadata_signature_nonce: PublicKey, + wallet_script_secret_key: String, + ) -> 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, + signatures, + total_script_pubkey, + total_offset_pubkey, + total_signature_nonce, + metadata_signature_nonce, + wallet_script_secret_key, + ) + .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 3a1bd1490df..08e9f16d1b7 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 ad0b1f17640..3b11f18c846 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 a620b82b92f..83163b5c1fb 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};