From 85a4d93242af4b95926a5a990764d6f0c1b95f8d Mon Sep 17 00:00:00 2001 From: Hansie Odendaal Date: Thu, 25 Jul 2024 05:06:00 +0200 Subject: [PATCH 1/3] Add pre-mine create Added pre-mine creation command-line commands for the creation ceremony and support within the ledger application. --- Cargo.lock | 31 +- .../minotari_console_wallet/Cargo.toml | 2 +- .../src/automation/commands.rs | 841 ++++++++++++++++-- .../src/automation/error.rs | 2 + .../src/automation/mod.rs | 45 +- .../src/automation/utils.rs | 80 +- .../minotari_console_wallet/src/cli.rs | 55 +- .../minotari_console_wallet/src/init/mod.rs | 42 +- .../src/wallet_modes.rs | 78 +- .../common/src/common_types.rs | 7 + .../minotari_ledger_wallet/comms/Cargo.toml | 1 + .../comms/examples/ledger_demo/main.rs | 28 +- .../comms/src/accessor_methods.rs | 20 +- .../minotari_ledger_wallet/wallet/src/main.rs | 2 + base_layer/common_types/src/key_branches.rs | 157 +++- base_layer/common_types/src/wallet_types.rs | 12 +- base_layer/core/Cargo.toml | 1 + base_layer/core/src/blocks/mod.rs | 2 +- base_layer/core/src/blocks/pre_mine/mod.rs | 685 +++++++++++--- .../src/transactions/key_manager/inner.rs | 34 +- .../src/transactions/key_manager/wrapper.rs | 5 + 21 files changed, 1779 insertions(+), 351 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8f30fa3620..4689ed4e4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1666,15 +1666,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - [[package]] name = "dirs-next" version = "1.0.2" @@ -1695,18 +1686,6 @@ dependencies = [ "dirs-sys-next", ] -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -3329,7 +3308,7 @@ dependencies = [ "console-subscriber", "crossterm 0.25.0", "digest 0.10.7", - "dirs", + "dirs-next 2.0.0", "futures 0.3.29", "log", "log4rs", @@ -3384,6 +3363,7 @@ dependencies = [ "dialoguer 0.11.0", "ledger-transport 0.10.0 (git+https://github.com/Zondax/ledger-rs?rev=20e2a20)", "ledger-transport-hid", + "log", "minotari_ledger_wallet_common", "once_cell", "rand 0.9.0-alpha.1", @@ -4038,12 +4018,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - [[package]] name = "ordered-float" version = "2.10.1" @@ -6250,6 +6224,7 @@ dependencies = [ "decimal-rs", "derivative", "digest 0.10.7", + "dirs-next 1.0.2", "env_logger 0.7.1", "fs2", "futures 0.3.29", diff --git a/applications/minotari_console_wallet/Cargo.toml b/applications/minotari_console_wallet/Cargo.toml index e048b3b813..e3aec2e9d3 100644 --- a/applications/minotari_console_wallet/Cargo.toml +++ b/applications/minotari_console_wallet/Cargo.toml @@ -38,7 +38,7 @@ clap = { version = "3.2", features = ["derive", "env"] } config = "0.14.0" crossterm = { version = "0.25.0" } digest = "0.10" -dirs = "5.0" +dirs-next = "2.0" futures = { version = "^0.3.16", default-features = false, features = [ "alloc", ] } diff --git a/applications/minotari_console_wallet/src/automation/commands.rs b/applications/minotari_console_wallet/src/automation/commands.rs index 44c6c43d2e..b417509cfd 100644 --- a/applications/minotari_console_wallet/src/automation/commands.rs +++ b/applications/minotari_console_wallet/src/automation/commands.rs @@ -27,6 +27,8 @@ use std::{ fs::File, io, io::{LineWriter, Write}, + iter::once, + ops::Deref, path::{Path, PathBuf}, time::{Duration, Instant}, }; @@ -53,12 +55,15 @@ use minotari_wallet::{ }; use serde::{de::DeserializeOwned, Serialize}; use sha2::Sha256; +use tari_common::configuration::Network; use tari_common_types::{ burnt_proof::BurntProof, emoji::EmojiId, + key_branches::TransactionKeyManagerBranch, tari_address::TariAddress, transaction::TxId, types::{Commitment, FixedHash, HashOutput, PrivateKey, PublicKey, Signature}, + wallet_types::WalletType, }; use tari_comms::{ connectivity::{ConnectivityEvent, ConnectivityRequester}, @@ -67,6 +72,7 @@ use tari_comms::{ }; use tari_comms_dht::{envelope::NodeDestination, DhtDiscoveryRequester}; use tari_core::{ + blocks::pre_mine::{create_pre_mine_genesis_block_file, get_pre_mine_items, PreMineItem}, covenants::Covenant, transactions::{ key_manager::TransactionKeyManagerInterface, @@ -74,6 +80,7 @@ use tari_core::{ transaction_components::{ encrypted_data::PaymentId, OutputFeatures, + RangeProofType, Transaction, TransactionInput, TransactionInputVersion, @@ -82,14 +89,15 @@ use tari_core::{ UnblindedOutput, WalletOutput, }, + CryptoFactories, }, }; use tari_crypto::{ keys::SecretKey, ristretto::{pedersen::PedersenCommitment, RistrettoSecretKey}, }; -use tari_key_manager::key_manager_service::KeyManagerInterface; -use tari_script::{script, CheckSigSchnorrSignature}; +use tari_key_manager::key_manager_service::{KeyId, KeyManagerInterface}; +use tari_script::{script, CheckSigSchnorrSignature, Opcode}; use tari_utilities::{encoding::Base58, hex::Hex, ByteArray}; use tokio::{ sync::{broadcast, mpsc}, @@ -101,20 +109,24 @@ use crate::{ automation::{ utils::{ get_file_name, + json_from_file_single_object, move_session_file_to_session_dir, out_dir, read_and_verify, + read_genesis_file, read_session_info, read_verify_session_info, write_json_object_to_file_as_line, write_to_json_file, + Context, }, - Step1SessionInfo, - Step2OutputsForLeader, - Step2OutputsForSelf, - Step3OutputsForParties, - Step3OutputsForSelf, - Step4OutputsForLeader, + PreMineCreateStep1ForLeader, + PreMineSpendStep1SessionInfo, + PreMineSpendStep2OutputsForLeader, + PreMineSpendStep2OutputsForSelf, + PreMineSpendStep3OutputsForParties, + PreMineSpendStep3OutputsForSelf, + PreMineSpendStep4OutputsForLeader, }, cli::{CliCommands, MakeItRainTransactionType}, utils::db::{CUSTOM_BASE_NODE_ADDRESS_KEY, CUSTOM_BASE_NODE_PUBLIC_KEY_KEY}, @@ -123,12 +135,14 @@ use crate::{ pub const LOG_TARGET: &str = "wallet::automation::commands"; // Pre-mine file names pub(crate) const FILE_EXTENSION: &str = "json"; -pub(crate) const SESSION_INFO: &str = "step_1_session_info"; -pub(crate) const STEP_2_LEADER: &str = "step_2_for_leader_from_"; -pub(crate) const STEP_2_SELF: &str = "step_2_for_self"; -pub(crate) const STEP_3_SELF: &str = "step_3_for_self"; -pub(crate) const STEP_3_PARTIES: &str = "step_3_for_parties"; -pub(crate) const STEP_4_LEADER: &str = "step_4_for_leader_from_"; +pub(crate) const CREATE_STEP_1_LEADER: &str = "step_1_for_leader_from_"; +pub(crate) const CREATE_STEP_2_LEADER: &str = "step_2_for_parties"; +pub(crate) const SPEND_SESSION_INFO: &str = "step_1_session_info"; +pub(crate) const SPEND_STEP_2_LEADER: &str = "step_2_for_leader_from_"; +pub(crate) const SPEND_STEP_2_SELF: &str = "step_2_for_self"; +pub(crate) const SPEND_STEP_3_SELF: &str = "step_3_for_self"; +pub(crate) const SPEND_STEP_3_PARTIES: &str = "step_3_for_parties"; +pub(crate) const SPEND_STEP_4_LEADER: &str = "step_4_for_leader_from_"; #[derive(Debug)] pub struct SentTransaction {} @@ -759,56 +773,487 @@ pub async fn command_runner( Err(e) => eprintln!("BurnMinotari error! {}", e), } }, - PreMineGenerateSessionInfo(args) => { - let commitment = if let Ok(val) = Commitment::from_hex(&args.commitment) { - val - } else { - eprintln!("\nError: Invalid 'commitment' provided!\n"); - continue; + PreMineCreateScriptInputs(args) => { + match key_manager_service.get_wallet_type().await { + WalletType::Ledger(_) => {}, + _ => { + eprintln!("\nError: Wallet type must be 'Ledger' to create pre-mine info!\n"); + break; + }, + } + + if args.alias.is_empty() || args.alias.contains(" ") { + eprintln!("\nError: Alias cannot contain spaces!\n"); + break; + } + if args.alias.chars().any(|c| !c.is_alphanumeric() && c != '_') { + eprintln!("\nError: Alias contains invalid characters! Only alphanumeric and '_' are allowed.\n"); + break; + } + + // Get the pre-mine items according to the unlock schedule specification + let pre_mine_items = match get_pre_mine_items().await { + Ok(items) => items, + Err(e) => { + eprintln!("\nError: {}\n", e); + break; + }, }; - let hash = if let Ok(val) = FixedHash::from_hex(&args.output_hash) { - val - } else { - eprintln!("\nError: Invalid 'output_hash' provided!\n"); - continue; + + let (session_id, out_dir) = match create_pre_mine_output_dir() { + Ok(values) => values, + Err(e) => { + eprintln!("\nError: {}\n", e); + break; + }, + }; + let out_file = out_dir.join(get_file_name(CREATE_STEP_1_LEADER, Some(args.alias.clone()))); + + let mut outputs_for_leader = Vec::with_capacity(pre_mine_items.len()); + let mut error = false; + for index in 0..pre_mine_items.len() as u64 { + let key_id = KeyId::Managed { + branch: TransactionKeyManagerBranch::PreMine.get_branch_key(), + index, + }; + let script_public_key = match key_manager_service.get_public_key_at_key_id(&key_id).await { + Ok(key) => key, + Err(e) => { + eprintln!("\nError: Could not retrieve script key for output {}: {}\n", index, e); + error = true; + break; + }, + }; + let signature = match key_manager_service + .sign_script_message(&key_id, PrivateKey::from(index).as_bytes()) + .await + { + Ok(value) => value, + Err(e) => { + eprintln!("\nError: Could not sign script message for output {}: {}\n", index, e); + error = true; + break; + }, + }; + outputs_for_leader.push(PreMineCreateStep1ForLeader { + index, + script_public_key, + verification_signature: signature, + }); + } + if error { + break; + } + write_to_json_file(&out_file, true, outputs_for_leader)?; + + println!(); + println!("Concluded step 1 'pre-mine-create-script-inputs'"); + println!("Your session ID is: '{}'", session_id); + println!("Your session's output directory is: '{}'", out_dir.display()); + println!("Session info saved to: '{}'", out_file.display()); + println!( + "Send '{}' to leader for step 2", + get_file_name(CREATE_STEP_1_LEADER, None) + ); + println!(); + }, + PreMineCreateGenesisFile(args) => { + // Read inputs from party members + let mut threshold_inputs = Vec::with_capacity(args.party_file_names.len()); + for file in &args.party_file_names { + threshold_inputs.push(json_from_file_single_object::<_, Vec>( + &file, None, + )?); + } + let backup_inputs = json_from_file_single_object::<_, Vec>( + &args.fail_safe_file_name, + None, + )?; + + // Get the pre-mine items according to the unlock schedule specification + let pre_mine_items = match get_pre_mine_items().await { + Ok(items) => items, + Err(e) => { + eprintln!("\nError: {}\n", e); + break; + }, + }; + + // Perform party members input verification + if let Err(e) = verify_script_pre_mine_inputs( + &threshold_inputs, + &backup_inputs, + &args.party_file_names, + &args.fail_safe_file_name, + &pre_mine_items, + ) { + eprintln!("\nError: {}\n", e); + break; + } + + // Extract the threshold and backup spend keys + let (threshold_spend_keys, backup_spend_keys, _all_spend_keys) = + match extract_threshold_and_backup_spend_keys(&threshold_inputs, &backup_inputs) { + Ok(keys) => keys, + Err(e) => { + eprintln!("\nError: {}\n", e); + break; + }, + }; + + // Create the pre-mine genesis block outputs and kernel + let (outputs, kernel) = match create_pre_mine_genesis_block_file( + &pre_mine_items, + &threshold_spend_keys, + &backup_spend_keys, + ) + .await + { + Ok(values) => values, + Err(e) => { + eprintln!("\nError: {}\n", e); + break; + }, + }; + + // Create the genesis file + let (session_id, out_dir) = match create_pre_mine_output_dir() { + Ok(values) => values, + Err(e) => { + eprintln!("\nError: {}\n", e); + break; + }, + }; + let file_name = match Network::get_current_or_user_setting_or_default() { + Network::MainNet => "mainnet_pre_mine.json", + Network::StageNet => "stagenet_pre_mine.json", + Network::NextNet => "nextnet_pre_mine.json", + Network::LocalNet => "esmeralda_pre_mine.json", + Network::Igor => "igor_pre_mine.json", + Network::Esmeralda => "esmeralda_pre_mine.json", + }; + let out_file = out_dir.join(file_name); + let mut file_stream = File::create(&out_file).expect("Could not create 'utxos.json'"); + + let mut error = false; + for output in outputs { + let utxo_s = match serde_json::to_string(&output) { + Ok(val) => val, + Err(e) => { + eprintln!("\nError: Could not serialize UTXO ({})\n", e); + error = true; + break; + }, + }; + if let Err(e) = file_stream.write_all(format!("{}\n", utxo_s).as_bytes()) { + eprintln!("\nError: Could not serialize UTXO ({})\n", e); + error = true; + break; + } + } + if error { + break; + } + let kernel = match serde_json::to_string(&kernel) { + Ok(val) => val, + Err(e) => { + eprintln!("\nError: Could not serialize kernel ({})\n", e); + break; + }, + }; + if let Err(e) = file_stream.write_all(format!("{}\n", kernel).as_bytes()) { + eprintln!("\nError: Could not write the genesis file ({})\n", e); + break; + } + + println!(); + println!("Concluded step 2 'pre-mine-create-genesis-file'"); + println!("Your session ID is: '{}'", session_id); + println!("Your session's output directory is: '{}'", out_dir.display()); + println!("Outputs written to: '{}'", out_file.display()); + println!( + "Send '{}' to parties for step 3", + get_file_name(CREATE_STEP_2_LEADER, None) + ); + println!(); + }, + PreMineCreateVerifyGenesisFile(args) => { + match key_manager_service.get_wallet_type().await { + WalletType::Ledger(_) => {}, + _ => { + eprintln!("\nError: Wallet type must be 'Ledger' to create pre-mine info!\n"); + break; + }, + } + + // Read inputs from party members + let mut threshold_inputs = Vec::with_capacity(args.party_file_names.len()); + let out_dir = out_dir(&args.session_id, Context::Create)?; + let party_files: Vec = args.party_file_names.iter().map(|v| out_dir.join(v)).collect(); + let mut error = false; + for file in &party_files { + let party_info = + match json_from_file_single_object::<_, Vec>(file, None) { + Ok(info) => info, + Err(e) => { + eprintln!("\nError: {}\n", e); + error = true; + break; + }, + }; + threshold_inputs.push(party_info); + } + if error { + break; + } + let fail_safe_file = out_dir.join(args.fail_safe_file_name); + let backup_inputs = + json_from_file_single_object::<_, Vec>(&fail_safe_file, None)?; + + // Read the pe-mine genesis file + let pre_mine_file = out_dir.join(args.pre_mine_file_name); + let (outputs, kernel) = match read_genesis_file(&pre_mine_file) { + Ok(items) => items, + Err(e) => { + eprintln!("\nError: {}\n", e); + break; + }, + }; + + // Verify the kernel signature + if let Err(e) = kernel.verify_signature() { + eprintln!("\nError: Kernel signature verification failed: {}\n", e); + break; + } + + // Get the pre-mine items according to the unlock schedule specification + let pre_mine_items = match get_pre_mine_items().await { + Ok(items) => items, + Err(e) => { + eprintln!("\nError: {}\n", e); + break; + }, + }; + + // Perform party members input verification + if let Err(e) = verify_script_pre_mine_inputs( + &threshold_inputs, + &backup_inputs, + &party_files, + &fail_safe_file, + &pre_mine_items, + ) { + eprintln!("\nError: {}\n", e); + break; + } + if outputs.len() != pre_mine_items.len() { + eprintln!( + "\nError: Mismatched number of outputs ({}) and pre-mine items ({})\n", + outputs.len(), + pre_mine_items.len() + ); + break; + } + + // Verify all outputs + let (threshold_spend_keys, backup_spend_keys, _all_party_keys) = + match extract_threshold_and_backup_spend_keys(&threshold_inputs, &backup_inputs) { + Ok(keys) => keys, + Err(e) => { + eprintln!("\nError: {}\n", e); + break; + }, + }; + + let range_proof_service = CryptoFactories::default().range_proof; + let mut error = false; + for (index, (((output, threshold_keys), backup_key), pre_mine_item)) in outputs + .iter() + .zip(threshold_spend_keys) + .zip(backup_spend_keys) + .zip(pre_mine_items) + .enumerate() + { + if pre_mine_item.value != output.minimum_value_promise { + eprintln!( + "\nError: Mismatched value for output {} ({} != {})\n", + index, pre_mine_item.value, output.minimum_value_promise + ); + error = true; + break; + } + if pre_mine_item.maturity != output.features.maturity { + eprintln!( + "\nError: Mismatched maturity for output {} ({} != {})\n", + index, pre_mine_item.maturity, output.features.maturity + ); + error = true; + break; + } + if output.features.range_proof_type != RangeProofType::RevealedValue { + eprintln!("\nError: Output {} does not have a RevealedValue range proof\n", index); + error = true; + break; + } + if let Err(e) = output.verify_metadata_signature() { + eprintln!( + "\nError: Output {} metadata signature verification failed: {}\n", + index, e + ); + error = true; + break; + } + if let Err(e) = output.verify_range_proof(&range_proof_service) { + eprintln!("\nError: Output {} range proof verification failed: {}\n", index, e); + error = true; + break; + } + // Retrieve the list of threshold keys and backup key from the script + let script_threshold_keys = + if let Some(Opcode::CheckMultiSigVerifyAggregatePubKey(_n, _m, keys, _msg)) = + output.script.as_slice().get(3) + { + keys.clone() + } else { + eprintln!( + "\nError: Output {} script does not contain a CheckMultiSigVerifyAggregatePubKey\n", + index + ); + error = true; + break; + }; + let script_backup_key = if let Some(Opcode::PushPubKey(key)) = output.script.as_slice().get(5) { + key.deref().clone() + } else { + eprintln!("\nError: Output {} script does not contain a PushPubKey\n", index); + error = true; + break; + }; + // Verify that the script keys correspond to the threshold and backup keys from the party members + let mut all_script_keys = script_threshold_keys + .iter() + .chain(once(&script_backup_key)) + .cloned() + .collect::>(); + let mut all_party_keys = threshold_keys + .iter() + .chain(once(&backup_key)) + .cloned() + .collect::>(); + all_script_keys.sort(); + all_party_keys.sort(); + if all_script_keys.len() != all_party_keys.len() { + eprintln!( + "\nError: Output {} script key count mismatch ({} != {})\n", + index, + all_script_keys.len(), + all_party_keys.len() + ); + error = true; + break; + } + all_script_keys.dedup(); + if all_party_keys.len() != all_script_keys.len() { + eprintln!("\nError: Output {} script keys not unique\n", index,); + error = true; + break; + } + for (index, (script_key, party_key)) in all_script_keys.iter().zip(all_party_keys).enumerate() { + if script_key != &party_key { + eprintln!( + "\nError: Output {} script key mismatch ({} != {})\n", + index, script_key, party_key + ); + error = true; + break; + } + } + if error { + break; + } + // Verify that script key owned by this wallet can be retrieved via the key id + let key_id = KeyId::Managed { + branch: TransactionKeyManagerBranch::PreMine.get_branch_key(), + index: index as u64, + }; + let expected_script_key = match key_manager_service.get_public_key_at_key_id(&key_id).await { + Ok(key) => key, + Err(e) => { + eprintln!("\nError: Could not retrieve script key for output {}: {}\n", index, e); + error = true; + break; + }, + }; + if !all_script_keys.iter().any(|k| k == &expected_script_key) { + eprintln!( + "\nError: Output {} script key mismatch ({} not found in script)\n", + index, expected_script_key + ); + error = true; + break; + } + } + if error { + break; + } + + println!(); + println!("Concluded step 3 'pre-mine-create-verify-genesis-file'"); + println!("Pre-mine file '{}' successfully verified", pre_mine_file.display()); + println!(); + }, + PreMineSpendSessionInfo(args) => { + let embedded_output = match get_embedded_pre_mine_outputs(vec![args.output_index]) { + Ok(outputs) => outputs[0].clone(), + Err(e) => { + eprintln!("\nError: {}\n", e); + break; + }, }; + let commitment = embedded_output.commitment.clone(); + let output_hash = embedded_output.hash(); if args.verify_unspent_outputs { - let unspent_outputs = transaction_service.fetch_unspent_outputs(vec![hash]).await?; + let unspent_outputs = transaction_service.fetch_unspent_outputs(vec![output_hash]).await?; if unspent_outputs.is_empty() { eprintln!( "\nError: Output with output_hash '{}' has already been spent!\n", - args.output_hash + output_hash ); - continue; + break; } if unspent_outputs[0].commitment() != &commitment { eprintln!( "\nError: Mismatched commitment '{}' and output_hash '{}'; not for the same output!\n", - args.commitment, args.output_hash + commitment.to_hex(), + output_hash ); - continue; + break; } } let mut session_id = PrivateKey::random(&mut OsRng).to_base58(); session_id.truncate(16); - let session_info = Step1SessionInfo { + let session_info = PreMineSpendStep1SessionInfo { session_id: session_id.clone(), - commitment_to_spend: args.commitment, - output_hash: args.output_hash, + commitment_to_spend: commitment.to_hex(), + output_hash: output_hash.to_hex(), recipient_address: args.recipient_address, fee_per_gram: args.fee_per_gram, + output_index: args.output_index, }; - let out_dir = out_dir(&session_info.session_id)?; - let out_file = out_dir.join(get_file_name(SESSION_INFO, None)); + let out_dir = out_dir(&session_info.session_id, Context::Spend)?; + let out_file = out_dir.join(get_file_name(SPEND_SESSION_INFO, None)); write_to_json_file(&out_file, true, session_info)?; println!(); println!("Concluded step 1 'pre-mine-generate-session-info'"); println!("Your session ID is: '{}'", session_id); println!("Your session's output directory is: '{}'", out_dir.display()); println!("Session info saved to: '{}'", out_file.display()); - println!("Send '{}' to parties for step 2", get_file_name(SESSION_INFO, None)); + println!( + "Send '{}' to parties for step 2", + get_file_name(SPEND_SESSION_INFO, None) + ); println!(); }, PreMineSpendBackupUtxo(args) => { @@ -842,13 +1287,14 @@ pub async fn command_runner( } }, PreMineCreatePartyDetails(args) => { + PreMineSpendPartyDetails(args) => { if args.alias.is_empty() || args.alias.contains(" ") { eprintln!("\nError: Alias cannot contain spaces!\n"); - continue; + break; } if args.alias.chars().any(|c| !c.is_alphanumeric() && c != '_') { eprintln!("\nError: Alias contains invalid characters! Only alphanumeric and '_' are allowed.\n"); - continue; + break; } let wallet_spend_key = wallet.key_manager_service.get_spend_key().await?; @@ -857,9 +1303,36 @@ pub async fn command_runner( let sender_offset_nonce = key_manager_service.get_random_key().await?; // Read session info - let session_info = read_session_info(args.input_file.clone())?; + let session_info = read_session_info::(args.input_file.clone())?; + + if session_info.output_index != args.output_index { + eprintln!( + "\nError: Mismatched output index from leader '{}' vs. '{}'\n", + session_info.output_index, args.output_index + ); + break; + } + let embedded_output = get_embedded_pre_mine_outputs(vec![args.output_index])?[0].clone(); + let commitment = embedded_output.commitment.clone(); + let output_hash = embedded_output.hash(); + + if session_info.commitment_to_spend != commitment.to_hex() { + eprintln!( + "\nError: Mismatched commitment from leader '{}' vs. '{}'!\n", + session_info.commitment_to_spend, + commitment.to_hex() + ); + break; + } + if session_info.output_hash != output_hash.to_hex() { + eprintln!( + "\nError: Mismatched output hash from leader '{}' vs. '{}'!\n", + session_info.output_hash, + output_hash.to_hex() + ); + break; + } - let commitment = Commitment::from_hex(&session_info.commitment_to_spend)?; let shared_secret = key_manager_service .get_diffie_hellman_shared_secret( &sender_offset_key.key_id, @@ -871,31 +1344,51 @@ pub async fn command_runner( .await?; let shared_secret_public_key = PublicKey::from_canonical_bytes(shared_secret.as_bytes())?; + let pre_mine_script_key_id = KeyId::Managed { + branch: TransactionKeyManagerBranch::PreMine.get_branch_key(), + index: args.output_index as u64, + }; + let pre_mine_public_script_key = match key_manager_service + .get_public_key_at_key_id(&pre_mine_script_key_id) + .await + { + Ok(key) => key, + Err(e) => { + eprintln!( + "\nError: Could not retrieve script key for output {}: {}\n", + args.output_index, e + ); + break; + }, + }; + let script_input_signature = key_manager_service .sign_script_message(&wallet_spend_key.key_id, commitment.as_bytes()) .await?; - let out_dir = out_dir(&session_info.session_id)?; - let step_2_outputs_for_leader = Step2OutputsForLeader { + let out_dir = out_dir(&session_info.session_id, Context::Spend)?; + let step_2_outputs_for_leader = PreMineSpendStep2OutputsForLeader { script_input_signature, wallet_public_spend_key: wallet_spend_key.pub_key, public_script_nonce_key: script_nonce_key.pub_key, public_sender_offset_key: sender_offset_key.pub_key, public_sender_offset_nonce_key: sender_offset_nonce.pub_key, dh_shared_secret_public_key: shared_secret_public_key, + pre_mine_public_script_key, }; - let out_file_leader = out_dir.join(get_file_name(STEP_2_LEADER, Some(args.alias.clone()))); + let out_file_leader = out_dir.join(get_file_name(SPEND_STEP_2_LEADER, Some(args.alias.clone()))); write_json_object_to_file_as_line(&out_file_leader, true, session_info.clone())?; write_json_object_to_file_as_line(&out_file_leader, false, step_2_outputs_for_leader)?; - let step_2_outputs_for_self = Step2OutputsForSelf { + let step_2_outputs_for_self = PreMineSpendStep2OutputsForSelf { alias: args.alias.clone(), wallet_spend_key_id: wallet_spend_key.key_id, script_nonce_key_id: script_nonce_key.key_id, sender_offset_key_id: sender_offset_key.key_id, sender_offset_nonce_key_id: sender_offset_nonce.key_id, + pre_mine_script_key_id, }; - let out_file_self = out_dir.join(get_file_name(STEP_2_SELF, None)); + let out_file_self = out_dir.join(get_file_name(SPEND_STEP_2_SELF, None)); write_json_object_to_file_as_line(&out_file_self, true, session_info.clone())?; write_json_object_to_file_as_line(&out_file_self, false, step_2_outputs_for_self)?; @@ -905,13 +1398,13 @@ pub async fn command_runner( move_session_file_to_session_dir(&session_info.session_id, &args.input_file)?; println!( "Send '{}' to leader for step 3", - get_file_name(STEP_2_LEADER, Some(args.alias)) + get_file_name(SPEND_STEP_2_LEADER, Some(args.alias)) ); println!(); }, - PreMineEncumberAggregateUtxo(args) => { + PreMineSpendEncumberAggregateUtxo(args) => { // Read session info - let session_info = read_verify_session_info(&args.session_id)?; + let session_info = read_verify_session_info::(&args.session_id)?; #[allow(clippy::mutable_key_type)] let mut input_shares = HashMap::new(); @@ -921,9 +1414,12 @@ pub async fn command_runner( let mut dh_shared_secret_shares = Vec::with_capacity(args.input_file_names.len()); for file_name in args.input_file_names { // Read party input - let party_info = - read_and_verify::(&args.session_id, &file_name, &session_info)?; - input_shares.insert(party_info.wallet_public_spend_key, party_info.script_input_signature); + let party_info = read_and_verify::( + &args.session_id, + &file_name, + &session_info, + )?; + input_shares.insert(party_info.pre_mine_public_script_key, party_info.script_input_signature); script_signature_public_nonces.push(party_info.public_script_nonce_key); sender_offset_public_key_shares.push(party_info.public_sender_offset_key); metadata_ephemeral_public_key_shares.push(party_info.public_sender_offset_nonce_key); @@ -952,13 +1448,13 @@ pub async fn command_runner( total_metadata_ephemeral_public_key, total_script_nonce, )) => { - let out_dir = out_dir(&args.session_id)?; - let step_3_outputs_for_self = Step3OutputsForSelf { tx_id }; - let out_file = out_dir.join(get_file_name(STEP_3_SELF, None)); + let out_dir = out_dir(&args.session_id, Context::Spend)?; + let step_3_outputs_for_self = PreMineSpendStep3OutputsForSelf { tx_id }; + let out_file = out_dir.join(get_file_name(SPEND_STEP_3_SELF, None)); write_json_object_to_file_as_line(&out_file, true, session_info.clone())?; write_json_object_to_file_as_line(&out_file, false, step_3_outputs_for_self)?; - let step_3_outputs_for_parties = Step3OutputsForParties { + let step_3_outputs_for_parties = PreMineSpendStep3OutputsForParties { input_stack: transaction.body.inputs()[0].clone().input_data, input_script: transaction.body.inputs()[0].script().unwrap().clone(), total_script_key: script_pubkey, @@ -977,31 +1473,34 @@ pub async fn command_runner( encrypted_data: transaction.body.outputs()[0].clone().encrypted_data, output_features: transaction.body.outputs()[0].clone().features, }; - let out_file = out_dir.join(get_file_name(STEP_3_PARTIES, None)); + let out_file = out_dir.join(get_file_name(SPEND_STEP_3_PARTIES, None)); write_json_object_to_file_as_line(&out_file, true, session_info.clone())?; write_json_object_to_file_as_line(&out_file, false, step_3_outputs_for_parties)?; println!(); println!("Concluded step 3 'pre-mine-encumber-aggregate-utxo'"); - println!("Send '{}' to parties for step 4", get_file_name(STEP_3_PARTIES, None)); + println!( + "Send '{}' to parties for step 4", + get_file_name(SPEND_STEP_3_PARTIES, None) + ); println!(); }, Err(e) => eprintln!("\nError: Encumber aggregate transaction error! {}\n", e), } }, - PreMineCreateInputOutputSigs(args) => { + PreMineSpendInputOutputSigs(args) => { // Read session info - let session_info = read_verify_session_info(&args.session_id)?; + let session_info = read_verify_session_info::(&args.session_id)?; // Read leader input - let leader_info = read_and_verify::( + let leader_info = read_and_verify::( &args.session_id, - &get_file_name(STEP_3_PARTIES, None), + &get_file_name(SPEND_STEP_3_PARTIES, None), &session_info, )?; // Read own party info - let party_info = read_and_verify::( + let party_info = read_and_verify::( &args.session_id, - &get_file_name(STEP_2_SELF, None), + &get_file_name(SPEND_STEP_2_SELF, None), &session_info, )?; @@ -1016,20 +1515,20 @@ pub async fn command_runner( &Commitment::from_hex(&session_info.commitment_to_spend)?, ); - let mut script_signature = Signature::default(); - match key_manager_service + let script_signature = match key_manager_service .sign_with_nonce_and_challenge( - &party_info.wallet_spend_key_id, + &party_info.pre_mine_script_key_id, &party_info.script_nonce_key_id, &challenge, ) .await { - Ok(signature) => { - script_signature = signature; + Ok(signature) => signature, + Err(e) => { + eprintln!("\nError: Script signature SignMessage error! {}\n", e); + break; }, - Err(e) => eprintln!("\nError: Script signature SignMessage error! {}\n", e), - } + }; // Metadata signature let script_offset = key_manager_service @@ -1052,8 +1551,7 @@ pub async fn command_runner( MicroMinotari::zero(), ); - let mut metadata_signature = Signature::default(); - match key_manager_service + let metadata_signature = match key_manager_service .sign_with_nonce_and_challenge( &party_info.sender_offset_key_id, &party_info.sender_offset_nonce_key_id, @@ -1061,25 +1559,27 @@ pub async fn command_runner( ) .await { - Ok(signature) => { - metadata_signature = signature; + Ok(signature) => signature, + Err(e) => { + eprintln!("\nError: Metadata signature SignMessage error! {}\n", e); + break; }, - Err(e) => eprintln!("\nError: Metadata signature SignMessage error! {}\n", e), - } + }; if script_signature.get_signature() == Signature::default().get_signature() || metadata_signature.get_signature() == Signature::default().get_signature() { - eprintln!("\nError: Script and/or metadata signatures not created!\n") + eprintln!("\nError: Script and/or metadata signatures not created!\n"); + break; } else { - let step_4_outputs_for_leader = Step4OutputsForLeader { + let step_4_outputs_for_leader = PreMineSpendStep4OutputsForLeader { script_signature, metadata_signature, script_offset, }; - let out_dir = out_dir(&args.session_id)?; - let out_file = out_dir.join(get_file_name(STEP_4_LEADER, Some(party_info.alias.clone()))); + let out_dir = out_dir(&args.session_id, Context::Spend)?; + let out_file = out_dir.join(get_file_name(SPEND_STEP_4_LEADER, Some(party_info.alias.clone()))); write_json_object_to_file_as_line(&out_file, true, session_info.clone())?; write_json_object_to_file_as_line(&out_file, false, step_4_outputs_for_leader)?; @@ -1087,31 +1587,34 @@ pub async fn command_runner( println!("Concluded step 4 'pre-mine-create-input-output-sigs'"); println!( "Send '{}' to leader for step 5", - get_file_name(STEP_4_LEADER, Some(party_info.alias)) + get_file_name(SPEND_STEP_4_LEADER, Some(party_info.alias)) ); println!(); } }, - PreMineSpendAggregateUtxo(args) => { + PreMineSpendAggregateTransaction(args) => { // Read session info - let session_info = read_verify_session_info(&args.session_id)?; + let session_info = read_verify_session_info::(&args.session_id)?; let mut metadata_signatures = Vec::with_capacity(args.input_file_names.len()); let mut script_signatures = Vec::with_capacity(args.input_file_names.len()); let mut offset = PrivateKey::default(); for file_name in args.input_file_names { // Read party input - let party_info = - read_and_verify::(&args.session_id, &file_name, &session_info)?; + let party_info = read_and_verify::( + &args.session_id, + &file_name, + &session_info, + )?; metadata_signatures.push(party_info.metadata_signature); script_signatures.push(party_info.script_signature); offset = &offset + &party_info.script_offset; } // Read own party info - let leader_info = read_and_verify::( + let leader_info = read_and_verify::( &args.session_id, - &get_file_name(STEP_3_SELF, None), + &get_file_name(SPEND_STEP_3_SELF, None), &session_info, )?; @@ -1652,6 +2155,170 @@ pub async fn command_runner( Ok(()) } +fn verify_script_pre_mine_inputs( + threshold_inputs: &[Vec], + backup_inputs: &[PreMineCreateStep1ForLeader], + party_file_names: &[PathBuf], + fail_safe_file_name: &Path, + pre_mine_items: &[PreMineItem], +) -> Result<(), String> { + for (k, party_info) in threshold_inputs.iter().enumerate() { + verify_party_script_inputs(&party_file_names[k], party_info, pre_mine_items)?; + } + verify_party_script_inputs(fail_safe_file_name, backup_inputs, pre_mine_items)?; + + // Ensure no keys for the same index are duplicated + let (_threshold_spend_keys, _backup_spend_keys, mut all_spend_keys) = + extract_threshold_and_backup_spend_keys(threshold_inputs, backup_inputs)?; + for (i, keys) in all_spend_keys.iter_mut().enumerate() { + let keys_len = keys.len(); + keys.sort(); + keys.dedup(); + if keys.len() != keys_len { + return Err(format!("Duplicate script keys for index '{}'!", i)); + } + } + // Ensure no keys for any index are duplicated + let mut all_spend_keys_flattened = all_spend_keys.into_iter().flatten().collect::>(); + all_spend_keys_flattened.sort(); + let all_spend_keys_len = all_spend_keys_flattened.len(); + all_spend_keys_flattened.dedup(); + if all_spend_keys_flattened.len() != all_spend_keys_len { + return Err("Duplicate script keys across parties!".to_string()); + } + + Ok(()) +} + +fn verify_party_script_inputs( + party_file_name: &Path, + party_info: &[PreMineCreateStep1ForLeader], + pre_mine_items: &[PreMineItem], +) -> Result<(), String> { + if party_info.len() != pre_mine_items.len() { + return Err(format!( + "Number of items in '{}' does not match the pre-mine items!", + party_file_name.display() + )); + } + // Ensure each key is unique + let mut script_keys = party_info + .iter() + .map(|v| v.script_public_key.clone()) + .collect::>(); + script_keys.sort(); + script_keys.dedup(); + if script_keys.len() != pre_mine_items.len() { + return Err(format!("Duplicate script keys in '{}'!", party_file_name.display())); + } + // Verify knowledge of the script private key + for (index, item) in party_info.iter().enumerate() { + if !item + .verification_signature + .verify(&item.script_public_key, PrivateKey::from(item.index).as_bytes()) + { + return Err(format!( + "Verification signature at index {} in '{}' is not valid!", + index, + party_file_name.display() + )); + } + if item.index != index as u64 { + return Err(format!( + "Index {} in '{}' does not align!", + index, + party_file_name.display() + )); + } + } + Ok(()) +} + +type PublicKeyVec = Vec; + +fn extract_threshold_and_backup_spend_keys( + threshold_inputs: &[Vec], + backup_inputs: &[PreMineCreateStep1ForLeader], +) -> Result<(Vec, PublicKeyVec, Vec), String> { + for item in threshold_inputs { + if item.is_empty() || item.len() != backup_inputs.len() { + return Err("Threshold/backup inputs empty or have different lengths!".to_string()); + } + } + let mut threshold_spend_keys = Vec::with_capacity(threshold_inputs[0].len()); + let mut backup_spend_keys = Vec::with_capacity(threshold_inputs[0].len()); + let mut all_spend_keys = Vec::with_capacity(threshold_inputs[0].len()); + for i in 0..threshold_inputs[0].len() { + let mut keys_for_round = Vec::with_capacity(threshold_inputs.len()); + for party_info in threshold_inputs { + keys_for_round.push(party_info[i].script_public_key.clone()); + } + threshold_spend_keys.push(keys_for_round.clone()); + backup_spend_keys.push(backup_inputs[i].clone().script_public_key); + keys_for_round.push(backup_inputs[i].clone().script_public_key); + all_spend_keys.push(keys_for_round); + } + Ok((threshold_spend_keys, backup_spend_keys, all_spend_keys)) +} + +fn create_pre_mine_output_dir() -> Result<(String, PathBuf), CommandError> { + let mut session_id = PrivateKey::random(&mut OsRng).to_base58(); + session_id.truncate(16); + let out_dir = out_dir(&session_id, Context::Create)?; + fs::create_dir_all(out_dir.clone()) + .map_err(|e| CommandError::JsonFile(format!("{} ({})", e, out_dir.display())))?; + Ok((session_id, out_dir)) +} + +fn get_embedded_pre_mine_outputs(output_indexes: Vec) -> Result, CommandError> { + let pre_mine_contents = match Network::get_current_or_user_setting_or_default() { + Network::MainNet => { + unimplemented!("MainNet pre-mine not yet implemented"); + }, + Network::StageNet => { + include_str!("../../../../base_layer/core/src/blocks/pre_mine/stagenet_pre_mine.json") + }, + Network::NextNet => { + include_str!("../../../../base_layer/core/src/blocks/pre_mine/nextnet_pre_mine.json") + }, + Network::LocalNet => { + include_str!("../../../../base_layer/core/src/blocks/pre_mine/esmeralda_pre_mine.json") + }, + Network::Igor => { + include_str!("../../../../base_layer/core/src/blocks/pre_mine/igor_pre_mine.json") + }, + Network::Esmeralda => { + include_str!("../../../../base_layer/core/src/blocks/pre_mine/esmeralda_pre_mine.json") + }, + }; + let mut utxos = Vec::new(); + let mut counter = 1; + let lines_count = pre_mine_contents.lines().count(); + for line in pre_mine_contents.lines() { + if counter < lines_count { + let utxo: TransactionOutput = + serde_json::from_str(line).map_err(|e| CommandError::PreMine(format!("{}", e)))?; + utxos.push(utxo); + } else { + break; + } + counter += 1; + } + + let mut fetched_outputs = Vec::with_capacity(output_indexes.len()); + for index in output_indexes { + if index >= utxos.len() { + return Err(CommandError::PreMine(format!( + "Error: Invalid 'output_index' {} provided pre-mine outputs only number {}!", + index, + utxos.len() + ))); + } + fetched_outputs.push(utxos[index].clone()); + } + Ok(fetched_outputs) +} + fn write_utxos_to_csv_file( utxos: Vec<(UnblindedOutput, Commitment)>, file_path: PathBuf, diff --git a/applications/minotari_console_wallet/src/automation/error.rs b/applications/minotari_console_wallet/src/automation/error.rs index 10887d3588..ceda7bf287 100644 --- a/applications/minotari_console_wallet/src/automation/error.rs +++ b/applications/minotari_console_wallet/src/automation/error.rs @@ -82,6 +82,8 @@ pub enum CommandError { IoError(#[from] io::Error), #[error("General error: {0}")] General(String), + #[error("Pre-mine error: {0}")] + PreMine(String), #[error("FixedHash size error `{0}`")] FixedHashSizeError(#[from] FixedHashSizeError), #[error("ByteArrayError {0}")] diff --git a/applications/minotari_console_wallet/src/automation/mod.rs b/applications/minotari_console_wallet/src/automation/mod.rs index 2c0fc39a55..27ab569548 100644 --- a/applications/minotari_console_wallet/src/automation/mod.rs +++ b/applications/minotari_console_wallet/src/automation/mod.rs @@ -39,46 +39,63 @@ use tari_core::transactions::{ }; use tari_script::{CheckSigSchnorrSignature, ExecutionStack, TariScript}; -// Outputs for self with `PreMineCreatePartyDetails` +// Step 1 outputs for leader with `PreMineCreateScriptInputs` #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] -struct Step1SessionInfo { +struct PreMineCreateStep1ForLeader { + pub index: u64, + script_public_key: PublicKey, + verification_signature: CheckSigSchnorrSignature, +} + +// Step 1 outputs for all with `PreMineSpendSessionInfo` +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +struct PreMineSpendStep1SessionInfo { session_id: String, fee_per_gram: MicroMinotari, commitment_to_spend: String, output_hash: String, recipient_address: TariAddress, + output_index: usize, } -// Outputs for self with `PreMineCreatePartyDetails` +impl SessionId for PreMineSpendStep1SessionInfo { + fn session_id(&self) -> String { + self.session_id.clone() + } +} + +// Step 2 outputs for self with `PreMineSpendPartyDetails` #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] -struct Step2OutputsForSelf { +struct PreMineSpendStep2OutputsForSelf { alias: String, wallet_spend_key_id: TariKeyId, script_nonce_key_id: TariKeyId, sender_offset_key_id: TariKeyId, sender_offset_nonce_key_id: TariKeyId, + pre_mine_script_key_id: TariKeyId, } -// Outputs for leader with `PreMineCreatePartyDetails` +// Step 2 outputs for leader with `PreMineSpendPartyDetails` #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] -struct Step2OutputsForLeader { +struct PreMineSpendStep2OutputsForLeader { script_input_signature: CheckSigSchnorrSignature, wallet_public_spend_key: PublicKey, public_script_nonce_key: PublicKey, public_sender_offset_key: PublicKey, public_sender_offset_nonce_key: PublicKey, dh_shared_secret_public_key: PublicKey, + pre_mine_public_script_key: PublicKey, } -// Outputs for self with `PreMineEncumberAggregateUtxo` +// Step 3 outputs for self with `PreMineSpendEncumberAggregateUtxo` #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] -struct Step3OutputsForSelf { +struct PreMineSpendStep3OutputsForSelf { tx_id: TxId, } -// Outputs for parties with `PreMineEncumberAggregateUtxo` +// Step 3 outputs for parties with `PreMineSpendEncumberAggregateUtxo` #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] -struct Step3OutputsForParties { +struct PreMineSpendStep3OutputsForParties { input_stack: ExecutionStack, input_script: TariScript, total_script_key: PublicKey, @@ -92,10 +109,14 @@ struct Step3OutputsForParties { output_features: OutputFeatures, } -// Outputs for leader with `PreMineCreateScriptSig` and `PreMineCreateMetaSig` +// Step 4 outputs for leader with `PreMineSpendInputOutputSigs` #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] -struct Step4OutputsForLeader { +struct PreMineSpendStep4OutputsForLeader { script_signature: Signature, metadata_signature: Signature, script_offset: PrivateKey, } + +trait SessionId { + fn session_id(&self) -> String; +} diff --git a/applications/minotari_console_wallet/src/automation/utils.rs b/applications/minotari_console_wallet/src/automation/utils.rs index fa8894e062..619561a91a 100644 --- a/applications/minotari_console_wallet/src/automation/utils.rs +++ b/applications/minotari_console_wallet/src/automation/utils.rs @@ -28,11 +28,13 @@ use std::{ }; use serde::{de::DeserializeOwned, Serialize}; +use tari_core::transactions::transaction_components::{TransactionKernel, TransactionOutput}; use crate::automation::{ - commands::{FILE_EXTENSION, SESSION_INFO}, + commands::{FILE_EXTENSION, SPEND_SESSION_INFO}, error::CommandError, - Step1SessionInfo, + PreMineSpendStep1SessionInfo, + SessionId, }; #[derive(Debug)] @@ -126,18 +128,26 @@ fn append_to_json_file, T: Serialize>(file: P, data: T) -> Result Ok(()) } +pub(crate) enum Context { + Create, + Spend, +} + /// Return the output directory for the session -pub(crate) fn out_dir(session_id: &str) -> Result { - let base_dir = dirs::cache_dir().ok_or(CommandError::InvalidArgument( +pub(crate) fn out_dir(session_id: &str, action: Context) -> Result { + let base_dir = dirs_next::document_dir().ok_or(CommandError::InvalidArgument( "Could not find cache directory".to_string(), ))?; - Ok(base_dir.join("tari_pre_mine").join(session_id)) + match action { + Context::Create => Ok(base_dir.join("tari_pre_mine").join("create").join(session_id)), + Context::Spend => Ok(base_dir.join("tari_pre_mine").join("spend").join(session_id)), + } } /// Move the session file to the session directory pub(crate) fn move_session_file_to_session_dir(session_id: &str, input_file: &PathBuf) -> Result<(), CommandError> { - let out_dir = out_dir(session_id)?; - let session_file = out_dir.join(get_file_name(SESSION_INFO, None)); + let out_dir = out_dir(session_id, Context::Spend)?; + let session_file = out_dir.join(get_file_name(SPEND_SESSION_INFO, None)); if input_file != &session_file { fs::copy(input_file.clone(), session_file.clone())?; fs::remove_file(input_file.clone())?; @@ -151,31 +161,31 @@ pub(crate) fn move_session_file_to_session_dir(session_id: &str, input_file: &Pa } /// Read the session info from the session directory and verify the supplied session ID -pub(crate) fn read_verify_session_info(session_id: &str) -> Result { - let file_path = out_dir(session_id)?.join(get_file_name(SESSION_INFO, None)); - let session_info = json_from_file_single_object::<_, Step1SessionInfo>(&file_path, None)?; - if session_info.session_id != session_id { +pub(crate) fn read_verify_session_info(session_id: &str) -> Result { + let file_path = out_dir(session_id, Context::Spend)?.join(get_file_name(SPEND_SESSION_INFO, None)); + let session_info = json_from_file_single_object::<_, T>(&file_path, None)?; + if session_info.session_id() != session_id { return Err(CommandError::InvalidArgument(format!( "Session ID in session info file '{}' mismatch", - get_file_name(SESSION_INFO, None) + get_file_name(SPEND_SESSION_INFO, None) ))); } Ok(session_info) } /// Read the session info from the session directory -pub(crate) fn read_session_info(session_file: PathBuf) -> Result { - json_from_file_single_object::<_, Step1SessionInfo>(&session_file, None) +pub(crate) fn read_session_info(session_file: PathBuf) -> Result { + json_from_file_single_object::<_, T>(&session_file, None) } /// Read the inputs from the session directory and verify the header pub(crate) fn read_and_verify( session_id: &str, file_name: &str, - session_info: &Step1SessionInfo, + session_info: &PreMineSpendStep1SessionInfo, ) -> Result { - let out_dir = out_dir(session_id)?; - let header = json_from_file_single_object::<_, Step1SessionInfo>( + let out_dir = out_dir(session_id, Context::Spend)?; + let header = json_from_file_single_object::<_, PreMineSpendStep1SessionInfo>( &out_dir.join(file_name), Some(PartialRead { lines_to_read: 1, @@ -213,3 +223,39 @@ pub(crate) fn get_file_name(stem: &str, suffix: Option) -> String { file_name.push_str(FILE_EXTENSION); file_name } + +pub(crate) fn read_genesis_file(file_path: &Path) -> Result<(Vec, TransactionKernel), CommandError> { + let file = File::open(file_path) + .map_err(|e| CommandError::PreMine(format!("Problem opening file '{}' ({})", file_path.display(), e)))?; + let reader = BufReader::new(file); + + let mut outputs = Vec::new(); + let mut kernel: Option = None; + + for line in reader.lines() { + let line = line.map_err(|e| { + CommandError::PreMine(format!( + "Problem reading line in file '{}' ({})", + file_path.display(), + e + )) + })?; + if let Ok(output) = serde_json::from_str::(&line) { + outputs.push(output); + } else if let Ok(k) = serde_json::from_str::(&line) { + kernel = Some(k); + } else { + eprintln!("Error: Could not deserialize line: {}", line); + } + } + if outputs.is_empty() { + return Err(CommandError::PreMine(format!( + "No outputs found in '{}'", + file_path.display() + ))); + } + let kernel = + kernel.ok_or_else(|| CommandError::PreMine(format!("No kernel found in '{}'", file_path.display())))?; + + Ok((outputs, kernel)) +} diff --git a/applications/minotari_console_wallet/src/cli.rs b/applications/minotari_console_wallet/src/cli.rs index 401ebaeaf2..f38d3cc337 100644 --- a/applications/minotari_console_wallet/src/cli.rs +++ b/applications/minotari_console_wallet/src/cli.rs @@ -116,11 +116,14 @@ pub enum CliCommands { GetBalance, SendMinotari(SendMinotariArgs), BurnMinotari(BurnMinotariArgs), - PreMineGenerateSessionInfo(PreMineGenerateSessionInfoArgs), - PreMineCreatePartyDetails(PreMineCreatePartyDetailsArgs), - PreMineEncumberAggregateUtxo(PreMineEncumberAggregateUtxoArgs), - PreMineCreateInputOutputSigs(PreMineCreateInputOutputSigArgs), - PreMineSpendAggregateUtxo(PreMineSpendAggregateUtxoArgs), + PreMineCreateScriptInputs(PreMineCreateScriptInputsArgs), + PreMineCreateGenesisFile(PreMineCreateGenesisFileArgs), + PreMineCreateVerifyGenesisFile(PreMineCreateVerifyGenesisFileArgs), + PreMineSpendSessionInfo(PreMineSpendSessionInfoArgs), + PreMineSpendPartyDetails(PreMineSpendPartyDetailsArgs), + PreMineSpendEncumberAggregateUtxo(PreMineSpendEncumberAggregateUtxoArgs), + PreMineSpendInputOutputSigs(PreMineSpendInputOutputSigArgs), + PreMineSpendAggregateTransaction(PreMineSpendAggregateTransactionArgs), PreMineSpendBackupUtxo(PreMineSpendBackupUtxoArgs), SendOneSidedToStealthAddress(SendMinotariArgs), MakeItRain(MakeItRainArgs), @@ -165,13 +168,37 @@ pub struct BurnMinotariArgs { } #[derive(Debug, Args, Clone)] -pub struct PreMineGenerateSessionInfoArgs { +pub struct PreMineCreateScriptInputsArgs { #[clap(long)] - pub fee_per_gram: MicroMinotari, + pub alias: String, +} + +#[derive(Debug, Args, Clone)] +pub struct PreMineCreateGenesisFileArgs { #[clap(long)] - pub commitment: String, + pub party_file_names: Vec, #[clap(long)] - pub output_hash: String, + pub fail_safe_file_name: PathBuf, +} + +#[derive(Debug, Args, Clone)] +pub struct PreMineCreateVerifyGenesisFileArgs { + #[clap(long)] + pub session_id: String, + #[clap(long)] + pub party_file_names: Vec, + #[clap(long)] + pub fail_safe_file_name: String, + #[clap(long)] + pub pre_mine_file_name: String, +} + +#[derive(Debug, Args, Clone)] +pub struct PreMineSpendSessionInfoArgs { + #[clap(long)] + pub fee_per_gram: MicroMinotari, + #[clap(long)] + pub output_index: usize, #[clap(long)] pub recipient_address: TariAddress, #[clap(long)] @@ -179,15 +206,17 @@ pub struct PreMineGenerateSessionInfoArgs { } #[derive(Debug, Args, Clone)] -pub struct PreMineCreatePartyDetailsArgs { +pub struct PreMineSpendPartyDetailsArgs { #[clap(long)] pub input_file: PathBuf, #[clap(long)] + pub output_index: usize, + #[clap(long)] pub alias: String, } #[derive(Debug, Args, Clone)] -pub struct PreMineEncumberAggregateUtxoArgs { +pub struct PreMineSpendEncumberAggregateUtxoArgs { #[clap(long)] pub session_id: String, #[clap(long)] @@ -195,13 +224,13 @@ pub struct PreMineEncumberAggregateUtxoArgs { } #[derive(Debug, Args, Clone)] -pub struct PreMineCreateInputOutputSigArgs { +pub struct PreMineSpendInputOutputSigArgs { #[clap(long)] pub session_id: String, } #[derive(Debug, Args, Clone)] -pub struct PreMineSpendAggregateUtxoArgs { +pub struct PreMineSpendAggregateTransactionArgs { #[clap(long)] pub session_id: String, #[clap(long)] diff --git a/applications/minotari_console_wallet/src/init/mod.rs b/applications/minotari_console_wallet/src/init/mod.rs index e580051c3f..b548e41d35 100644 --- a/applications/minotari_console_wallet/src/init/mod.rs +++ b/applications/minotari_console_wallet/src/init/mod.rs @@ -51,6 +51,7 @@ use tari_common::{ exit_codes::{ExitCode, ExitError}, }; use tari_common_types::{ + key_branches::TransactionKeyManagerBranch, types::{PrivateKey, PublicKey}, wallet_types::{LedgerWallet, WalletType}, }; @@ -62,10 +63,18 @@ use tari_comms::{ }; use tari_core::{ consensus::ConsensusManager, - transactions::{transaction_components::TransactionError, CryptoFactories}, + transactions::{ + key_manager::{TariKeyId, TransactionKeyManagerInterface}, + transaction_components::TransactionError, + CryptoFactories, + }, }; use tari_crypto::{keys::PublicKey as PublicKeyTrait, ristretto::RistrettoPublicKey}; -use tari_key_manager::{cipher_seed::CipherSeed, mnemonic::MnemonicLanguage}; +use tari_key_manager::{ + cipher_seed::CipherSeed, + key_manager_service::{storage::database::KeyManagerBackend, KeyManagerInterface}, + mnemonic::MnemonicLanguage, +}; use tari_p2p::{peer_seeds::SeedPeer, TransportType}; use tari_shutdown::ShutdownSignal; use tari_utilities::{hex::Hex, ByteArray, SafePassword}; @@ -561,6 +570,35 @@ pub async fn start_wallet( base_node: &Peer, wallet_mode: &WalletMode, ) -> Result<(), ExitError> { + // Verify ledger build if wallet type is Ledger + if let WalletType::Ledger(_) = wallet.key_manager_service.get_wallet_type().await { + #[cfg(not(feature = "ledger"))] + { + return Err(ExitError::new( + ExitCode::WalletError, + "Ledger is not supported in this build, please enable the \"ledger\" feature for console wallet and \ + core" + .to_string(), + )); + } + + #[cfg(feature = "ledger")] + { + let key_id = TariKeyId::Managed { + branch: TransactionKeyManagerBranch::RandomKey.get_branch_key(), + index: 0, + }; + match wallet.key_manager_service.get_public_key_at_key_id(&key_id).await { + Ok(public_key) => {}, + Err(e) => { + if e.to_string().contains("Ledger is not supported in this build") { + return Err(ExitError::new(ExitCode::WalletError, format!(" {}", e))); + } + }, + } + } + } + debug!(target: LOG_TARGET, "Setting base node peer"); let net_address = base_node diff --git a/applications/minotari_console_wallet/src/wallet_modes.rs b/applications/minotari_console_wallet/src/wallet_modes.rs index 31666865ce..c75440dd16 100644 --- a/applications/minotari_console_wallet/src/wallet_modes.rs +++ b/applications/minotari_console_wallet/src/wallet_modes.rs @@ -170,11 +170,14 @@ pub(crate) fn command_mode( fn force_exit_for_pre_mine_commands(command: &CliCommands) -> bool { matches!( command, - CliCommands::PreMineGenerateSessionInfo(_) | - CliCommands::PreMineEncumberAggregateUtxo(_) | - CliCommands::PreMineSpendAggregateUtxo(_) | - CliCommands::PreMineCreatePartyDetails(_) | - CliCommands::PreMineCreateInputOutputSigs(_) + CliCommands::PreMineCreateScriptInputs(_) | + CliCommands::PreMineCreateGenesisFile(_) | + CliCommands::PreMineCreateVerifyGenesisFile(_) | + CliCommands::PreMineSpendSessionInfo(_) | + CliCommands::PreMineSpendEncumberAggregateUtxo(_) | + CliCommands::PreMineSpendAggregateTransaction(_) | + CliCommands::PreMineSpendPartyDetails(_) | + CliCommands::PreMineSpendInputOutputSigs(_) ) } @@ -529,22 +532,32 @@ mod test { burn-minotari --message Ups_these_funds_will_be_burned! 100T - pre-mine-generate-session-info --fee-per-gram 2 --commitment \ - f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 --output-hash \ - f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 --recipient-address \ + pre-mine-create-script-inputs --alias alice + + pre-mine-create-genesis-file --party-file-names=step_1_for_leader_from_alice.json \ + --party-file-names=step_1_for_leader_from_bob.json --party-file-names=step_1_for_leader_from_carol.json \ + --fail-safe-file-name step_1_for_leader_from_dave.json + + pre-mine-create-verify-genesis-file --session-id ee1643655c \ + --party-file-names=step_1_for_leader_from_alice.json --party-file-names=step_1_for_leader_from_bob.json \ + --party-file-names=step_1_for_leader_from_carol.json --fail-safe-file-name \ + step_1_for_leader_from_dave.json --pre-mine-file-name ./step_2_for_parties.json + + pre-mine-spend-session-info --fee-per-gram 2 --output-index 123 --recipient-address \ f4LR9f6WwwcPiKJjK5ciTkU1ocNhANa3FPw1wkyVUwbuKpgiihawCXy6PFszunUWQ4Te8KVFnyWVHHwsk9x5Cg7ZQiA \ --verify-unspent-outputs - pre-mine-create-party-details --input-file ./step_1_session_info.txt --alias alice + pre-mine-spend-party-details --input-file ./step_1_session_info.txt --output-index 123 --alias alice - pre-mine-encumber-aggregate-utxo --session-id ee1643655c \ + pre-mine-spend-encumber-aggregate-utxo --session-id ee1643655c \ --input-file-names=step_2_for_leader_from_alice.txt --input-file-names=step_2_for_leader_from_bob.txt \ --input-file-names=step_2_for_leader_from_carol.txt - pre-mine-create-input-output-sigs --session-id ee1643655c + pre-mine-spend-input-output-sigs --session-id ee1643655c - pre-mine-spend-aggregate-utxo --session-id ee1643655c --input-file-names=step_4_for_leader_from_alice.txt \ - --input-file-names=step_4_for_leader_from_bob.txt --input-file-names=step_4_for_leader_from_carol.txt + pre-mine-spend-aggregate-transaction --session-id ee1643655c \ + --input-file-names=step_4_for_leader_from_alice.txt --input-file-names=step_4_for_leader_from_bob.txt \ + --input-file-names=step_4_for_leader_from_carol.txt coin-split --message Make_many_dust_UTXOs! --fee-per-gram 2 0.001T 499 @@ -565,11 +578,14 @@ mod test { let mut get_balance = false; let mut send_tari = false; let mut burn_tari = false; - let mut pre_mine_generate_session_info = false; - let mut pre_mine_encumber_aggregate_utxo = false; - let mut pre_mine_spend_aggregate_utxo = false; - let mut pre_mine_create_party_details = false; - let mut pre_mine_create_input_output_sigs = false; + let mut pre_mine_create_script_inputs = false; + let mut pre_mine_create_genesis_file = false; + let mut pre_mine_create_verify_genesis_file = false; + let mut pre_mine_spend_session_info = false; + let mut pre_mine_spend_encumber_aggregate_utxo = false; + let mut pre_mine_spend_aggregate_transaction = false; + let mut pre_mine_spend_party_details = false; + let mut pre_mine_spend_input_output_sigs = false; let mut make_it_rain = false; let mut coin_split = false; let mut discover_peer = false; @@ -581,11 +597,14 @@ mod test { CliCommands::GetBalance => get_balance = true, CliCommands::SendMinotari(_) => send_tari = true, CliCommands::BurnMinotari(_) => burn_tari = true, - CliCommands::PreMineGenerateSessionInfo(_) => pre_mine_generate_session_info = true, - CliCommands::PreMineEncumberAggregateUtxo(_) => pre_mine_encumber_aggregate_utxo = true, - CliCommands::PreMineSpendAggregateUtxo(_) => pre_mine_spend_aggregate_utxo = true, - CliCommands::PreMineCreatePartyDetails(_) => pre_mine_create_party_details = true, - CliCommands::PreMineCreateInputOutputSigs(_) => pre_mine_create_input_output_sigs = true, + CliCommands::PreMineCreateScriptInputs(_) => pre_mine_create_script_inputs = true, + CliCommands::PreMineCreateGenesisFile(_) => pre_mine_create_genesis_file = true, + CliCommands::PreMineCreateVerifyGenesisFile(_) => pre_mine_create_verify_genesis_file = true, + CliCommands::PreMineSpendSessionInfo(_) => pre_mine_spend_session_info = true, + CliCommands::PreMineSpendPartyDetails(_) => pre_mine_spend_party_details = true, + CliCommands::PreMineSpendEncumberAggregateUtxo(_) => pre_mine_spend_encumber_aggregate_utxo = true, + CliCommands::PreMineSpendInputOutputSigs(_) => pre_mine_spend_input_output_sigs = true, + CliCommands::PreMineSpendAggregateTransaction(_) => pre_mine_spend_aggregate_transaction = true, CliCommands::SendOneSidedToStealthAddress(_) => {}, CliCommands::MakeItRain(_) => make_it_rain = true, CliCommands::CoinSplit(_) => coin_split = true, @@ -621,11 +640,14 @@ mod test { get_balance && send_tari && burn_tari && - pre_mine_generate_session_info && - pre_mine_encumber_aggregate_utxo && - pre_mine_spend_aggregate_utxo && - pre_mine_create_party_details && - pre_mine_create_input_output_sigs && + pre_mine_create_script_inputs && + pre_mine_create_genesis_file && + pre_mine_create_verify_genesis_file && + pre_mine_spend_session_info && + pre_mine_spend_encumber_aggregate_utxo && + pre_mine_spend_aggregate_transaction && + pre_mine_spend_party_details && + pre_mine_spend_input_output_sigs && make_it_rain && coin_split && discover_peer && diff --git a/applications/minotari_ledger_wallet/common/src/common_types.rs b/applications/minotari_ledger_wallet/common/src/common_types.rs index 6ba10f1e7c..755c7fcf59 100644 --- a/applications/minotari_ledger_wallet/common/src/common_types.rs +++ b/applications/minotari_ledger_wallet/common/src/common_types.rs @@ -121,6 +121,7 @@ pub enum Branch { OneSidedSenderOffset = 0x06, Spend = 0x07, RandomKey = 0x08, + PreMine = 0x09, } impl Branch { @@ -139,6 +140,7 @@ impl Branch { 0x06 => Some(Branch::OneSidedSenderOffset), 0x07 => Some(Branch::Spend), 0x08 => Some(Branch::RandomKey), + 0x09 => Some(Branch::PreMine), _ => None, } } @@ -294,6 +296,7 @@ mod test { (0x06, Branch::OneSidedSenderOffset), (0x07, Branch::Spend), (0x08, Branch::RandomKey), + (0x09, Branch::PreMine), ]; for (expected_byte, branch) in &mappings { @@ -334,6 +337,10 @@ mod test { assert_eq!(branch.as_byte(), *expected_byte); assert_eq!(Branch::from_byte(*expected_byte), Some(*branch)); }, + Branch::PreMine => { + assert_eq!(branch.as_byte(), *expected_byte); + assert_eq!(Branch::from_byte(*expected_byte), Some(*branch)); + }, } } } diff --git a/applications/minotari_ledger_wallet/comms/Cargo.toml b/applications/minotari_ledger_wallet/comms/Cargo.toml index 643426d0b3..e3ea3bfb47 100644 --- a/applications/minotari_ledger_wallet/comms/Cargo.toml +++ b/applications/minotari_ledger_wallet/comms/Cargo.toml @@ -22,3 +22,4 @@ thiserror = "1.0.26" rand = "0.9.0-alpha.1" once_cell = "1.19.0" +log = "0.4.20" diff --git a/applications/minotari_ledger_wallet/comms/examples/ledger_demo/main.rs b/applications/minotari_ledger_wallet/comms/examples/ledger_demo/main.rs index 6166f75b20..6197f52c40 100644 --- a/applications/minotari_ledger_wallet/comms/examples/ledger_demo/main.rs +++ b/applications/minotari_ledger_wallet/comms/examples/ledger_demo/main.rs @@ -114,14 +114,34 @@ fn main() { // GetPublicKey println!("\ntest: GetPublicKey"); let index = OsRng.next_u64(); - let branch = TransactionKeyManagerBranch::RandomKey; + for branch in &[ + TransactionKeyManagerBranch::SenderOffsetLedger, + TransactionKeyManagerBranch::Spend, + TransactionKeyManagerBranch::RandomKey, + TransactionKeyManagerBranch::PreMine, + ] { + match ledger_get_public_key(account, index, *branch) { + Ok(public_key) => println!("public_key: {}", public_key.to_hex()), + Err(e) => { + println!("\nError: {}\n", e); + return; + }, + } + } + + let branch = TransactionKeyManagerBranch::CommitmentMask; match ledger_get_public_key(account, index, branch) { - Ok(public_key) => println!("public_key: {}", public_key.to_hex()), - Err(e) => { - println!("\nError: {}\n", e); + Ok(_public_key) => { + println!("\nError: Should not have returned a public key for '{:?}'\n", branch); return; }, + Err(e) => { + if e != LedgerDeviceError::Processing("GetPublicKey: expected 33 bytes, got 0 (BadBranchKey)".to_string()) { + println!("\nError: Unexpected response ({})\n", e); + return; + } + }, } // GetScriptSignature diff --git a/applications/minotari_ledger_wallet/comms/src/accessor_methods.rs b/applications/minotari_ledger_wallet/comms/src/accessor_methods.rs index 1fe4c122d0..55244fec62 100644 --- a/applications/minotari_ledger_wallet/comms/src/accessor_methods.rs +++ b/applications/minotari_ledger_wallet/comms/src/accessor_methods.rs @@ -22,6 +22,7 @@ use std::sync::Mutex; +use log::debug; use minotari_ledger_wallet_common::common_types::{AppSW, Instruction}; use once_cell::sync::Lazy; use rand::{rngs::OsRng, RngCore}; @@ -39,8 +40,7 @@ use crate::{ ledger_wallet::{Command, EXPECTED_NAME, EXPECTED_VERSION}, }; -// hash_domain!(CheckSigHashDomain, "com.tari.script.check_sig", 1); -// type CheckSigSchnorrSignature = SchnorrSignature; +const LOG_TARGET: &str = "ledger_wallet::accessor_methods"; /// Verify that the ledger application is working properly. pub fn verify_ledger_application() -> Result<(), LedgerDeviceError> { @@ -184,13 +184,14 @@ pub fn ledger_get_version() -> Result { /// Get the public alpha key from the ledger device pub fn ledger_get_public_spend_key(account: u64) -> Result { + debug!(target: LOG_TARGET, "ledger_get_public_spend_key: account {}", account); verify_ledger_application()?; match Command::>::build_command(account, Instruction::GetPublicSpendKey, vec![]).execute() { Ok(result) => { if result.data().len() < 33 { return Err(LedgerDeviceError::Processing(format!( - "GetPublicAlpha: expected 33 bytes, got {} ({:?})", + "GetPublicSpendKey: expected 33 bytes, got {} ({:?})", result.data().len(), AppSW::try_from(result.retcode())? ))); @@ -198,7 +199,7 @@ pub fn ledger_get_public_spend_key(account: u64) -> Result Err(LedgerDeviceError::Processing(format!("GetPublicAlpha: {}", e))), + Err(e) => Err(LedgerDeviceError::Processing(format!("GetPublicSpendKey: {}", e))), } } @@ -208,6 +209,7 @@ pub fn ledger_get_public_key( index: u64, branch: TransactionKeyManagerBranch, ) -> Result { + debug!(target: LOG_TARGET, "ledger_get_public_key: account {}, index {}, branch {:?}", account, index, branch); verify_ledger_application()?; let mut data = Vec::new(); @@ -219,7 +221,7 @@ pub fn ledger_get_public_key( Ok(result) => { if result.data().len() < 33 { return Err(LedgerDeviceError::Processing(format!( - "GetPublicAlpha: expected 33 bytes, got {} ({:?})", + "GetPublicKey: expected 33 bytes, got {} ({:?})", result.data().len(), AppSW::try_from(result.retcode())? ))); @@ -242,6 +244,7 @@ pub fn ledger_get_script_signature( commitment: &Commitment, script_message: [u8; 32], ) -> Result { + debug!(target: LOG_TARGET, "ledger_get_script_signature: account {}", account); verify_ledger_application()?; let mut data = Vec::new(); @@ -289,6 +292,7 @@ pub fn ledger_get_script_offset( derived_key_commitments: &[PrivateKey], sender_offset_indexes: &[u64], ) -> Result { + debug!(target: LOG_TARGET, "ledger_get_script_offset: account {}", account); verify_ledger_application()?; let num_commitments = derived_key_commitments.len() as u64; @@ -336,6 +340,7 @@ pub fn ledger_get_script_offset( /// Get the view key from the ledger device pub fn ledger_get_view_key(account: u64) -> Result { + debug!(target: LOG_TARGET, "ledger_get_view_key: account {}", account); verify_ledger_application()?; match Command::>::build_command(account, Instruction::GetViewKey, vec![]).execute() { @@ -361,6 +366,7 @@ pub fn ledger_get_dh_shared_secret( branch: TransactionKeyManagerBranch, public_key: &PublicKey, ) -> Result, LedgerDeviceError> { + debug!(target: LOG_TARGET, "ledger_get_dh_shared_secret: account {}, index {}, branch {:?}", account, index, branch); verify_ledger_application()?; let mut data = Vec::new(); @@ -393,6 +399,8 @@ pub fn ledger_get_raw_schnorr_signature( nonce_branch: TransactionKeyManagerBranch, challenge: &[u8; 64], ) -> Result { + debug!(target: LOG_TARGET, "ledger_get_raw_schnorr_signature: account {}, pk index {}, pk branch {:?}, nonce index {}, nonce branch {:?}", + account, private_key_index, private_key_branch, nonce_index, nonce_branch); verify_ledger_application()?; let mut data = Vec::new(); @@ -429,6 +437,8 @@ pub fn ledger_get_script_schnorr_signature( private_key_branch: TransactionKeyManagerBranch, nonce: &[u8], ) -> Result { + debug!(target: LOG_TARGET, "ledger_get_raw_schnorr_signature: account {}, pk index {}, pk branch {:?}", + account, private_key_index, private_key_branch); verify_ledger_application()?; let mut data = Vec::new(); diff --git a/applications/minotari_ledger_wallet/wallet/src/main.rs b/applications/minotari_ledger_wallet/wallet/src/main.rs index 27721d0672..9fbf390825 100644 --- a/applications/minotari_ledger_wallet/wallet/src/main.rs +++ b/applications/minotari_ledger_wallet/wallet/src/main.rs @@ -146,6 +146,7 @@ pub enum KeyType { ViewKey = 0x03, OneSidedSenderOffset = 0x04, Random = 0x06, + PreMine = 0x07, } impl KeyType { @@ -162,6 +163,7 @@ impl KeyType { BranchMapping::OneSidedSenderOffset => Ok(Self::OneSidedSenderOffset), BranchMapping::Spend => Ok(Self::Spend), BranchMapping::RandomKey => Ok(Self::Random), + BranchMapping::PreMine => Ok(Self::PreMine), _ => Err(AppSW::BadBranchKey), } } else { diff --git a/base_layer/common_types/src/key_branches.rs b/base_layer/common_types/src/key_branches.rs index 60c830d695..c90b684731 100644 --- a/base_layer/common_types/src/key_branches.rs +++ b/base_layer/common_types/src/key_branches.rs @@ -26,7 +26,7 @@ use strum_macros::EnumIter; use crate::WALLET_COMMS_AND_SPEND_KEY_BRANCH; #[repr(u8)] -#[derive(Clone, Copy, EnumIter)] +#[derive(Clone, Copy, EnumIter, Eq, PartialEq, Debug)] // These byte reps must stay in sync with the ledger representations at: // applications/minotari_ledger_wallet/wallet/src/main.rs pub enum TransactionKeyManagerBranch { @@ -39,6 +39,7 @@ pub enum TransactionKeyManagerBranch { OneSidedSenderOffset = Branch::OneSidedSenderOffset as u8, Spend = Branch::Spend as u8, RandomKey = Branch::RandomKey as u8, + PreMine = Branch::PreMine as u8, } const DATA_ENCRYPTION: &str = "data encryption"; @@ -49,6 +50,7 @@ const KERNEL_NONCE: &str = "kernel nonce"; const SENDER_OFFSET: &str = "sender offset"; const ONE_SIDED_SENDER_OFFSET: &str = "one sided sender offset"; const RANDOM_KEY: &str = "random key"; +const PRE_MINE: &str = "pre-mine"; impl TransactionKeyManagerBranch { /// Warning: Changing these strings will affect the backwards compatibility of the wallet with older databases or @@ -64,6 +66,7 @@ impl TransactionKeyManagerBranch { TransactionKeyManagerBranch::OneSidedSenderOffset => ONE_SIDED_SENDER_OFFSET.to_string(), TransactionKeyManagerBranch::RandomKey => RANDOM_KEY.to_string(), TransactionKeyManagerBranch::Spend => WALLET_COMMS_AND_SPEND_KEY_BRANCH.to_string(), + TransactionKeyManagerBranch::PreMine => PRE_MINE.to_string(), } } @@ -78,6 +81,7 @@ impl TransactionKeyManagerBranch { ONE_SIDED_SENDER_OFFSET => TransactionKeyManagerBranch::OneSidedSenderOffset, RANDOM_KEY => TransactionKeyManagerBranch::RandomKey, WALLET_COMMS_AND_SPEND_KEY_BRANCH => TransactionKeyManagerBranch::Spend, + PRE_MINE => TransactionKeyManagerBranch::PreMine, _ => TransactionKeyManagerBranch::Nonce, } } @@ -85,4 +89,155 @@ impl TransactionKeyManagerBranch { pub fn as_byte(self) -> u8 { self as u8 } + + pub fn from_byte(value: u8) -> Option { + match Branch::from_byte(value) { + Some(Branch::DataEncryption) => Some(TransactionKeyManagerBranch::DataEncryption), + Some(Branch::MetadataEphemeralNonce) => Some(TransactionKeyManagerBranch::MetadataEphemeralNonce), + Some(Branch::CommitmentMask) => Some(TransactionKeyManagerBranch::CommitmentMask), + Some(Branch::Nonce) => Some(TransactionKeyManagerBranch::Nonce), + Some(Branch::KernelNonce) => Some(TransactionKeyManagerBranch::KernelNonce), + Some(Branch::SenderOffset) => Some(TransactionKeyManagerBranch::SenderOffset), + Some(Branch::SenderOffsetLedger) => Some(TransactionKeyManagerBranch::SenderOffsetLedger), + Some(Branch::Spend) => Some(TransactionKeyManagerBranch::Spend), + Some(Branch::RandomKey) => Some(TransactionKeyManagerBranch::RandomKey), + Some(Branch::PreMine) => Some(TransactionKeyManagerBranch::PreMine), + None => None, + } + } +} + +#[cfg(test)] +mod test { + use minotari_ledger_wallet_common::common_types::Branch; + + use crate::{ + key_branches::{ + TransactionKeyManagerBranch, + COMMITMENT_MASK, + DATA_ENCRYPTION, + KERNEL_NONCE, + METADATA_EPHEMERAL_NONCE, + NONCE, + PRE_MINE, + RANDOM_KEY, + SENDER_OFFSET, + SENDER_OFFSET_LEDGER, + }, + WALLET_COMMS_AND_SPEND_KEY_BRANCH, + }; + + #[test] + #[allow(clippy::too_many_lines)] + fn test_branch_conversion() { + let mappings = [ + ( + Branch::DataEncryption as u8, + TransactionKeyManagerBranch::DataEncryption, + DATA_ENCRYPTION, + ), + ( + Branch::MetadataEphemeralNonce as u8, + TransactionKeyManagerBranch::MetadataEphemeralNonce, + METADATA_EPHEMERAL_NONCE, + ), + ( + Branch::CommitmentMask as u8, + TransactionKeyManagerBranch::CommitmentMask, + COMMITMENT_MASK, + ), + (Branch::Nonce as u8, TransactionKeyManagerBranch::Nonce, NONCE), + ( + Branch::KernelNonce as u8, + TransactionKeyManagerBranch::KernelNonce, + KERNEL_NONCE, + ), + ( + Branch::SenderOffset as u8, + TransactionKeyManagerBranch::SenderOffset, + SENDER_OFFSET, + ), + ( + Branch::SenderOffsetLedger as u8, + TransactionKeyManagerBranch::SenderOffsetLedger, + SENDER_OFFSET_LEDGER, + ), + ( + Branch::Spend as u8, + TransactionKeyManagerBranch::Spend, + WALLET_COMMS_AND_SPEND_KEY_BRANCH, + ), + ( + Branch::RandomKey as u8, + TransactionKeyManagerBranch::RandomKey, + RANDOM_KEY, + ), + (Branch::PreMine as u8, TransactionKeyManagerBranch::PreMine, PRE_MINE), + ]; + + for (expected_byte, branch, key) in &mappings { + match branch { + TransactionKeyManagerBranch::DataEncryption => { + assert_eq!(branch.as_byte(), *expected_byte); + assert_eq!(TransactionKeyManagerBranch::from_byte(*expected_byte), Some(*branch)); + assert_eq!(&branch.get_branch_key(), *key); + assert_eq!(TransactionKeyManagerBranch::from_key(key), *branch); + }, + TransactionKeyManagerBranch::MetadataEphemeralNonce => { + assert_eq!(branch.as_byte(), *expected_byte); + assert_eq!(TransactionKeyManagerBranch::from_byte(*expected_byte), Some(*branch)); + assert_eq!(&branch.get_branch_key(), *key); + assert_eq!(TransactionKeyManagerBranch::from_key(key), *branch); + }, + TransactionKeyManagerBranch::CommitmentMask => { + assert_eq!(branch.as_byte(), *expected_byte); + assert_eq!(TransactionKeyManagerBranch::from_byte(*expected_byte), Some(*branch)); + assert_eq!(&branch.get_branch_key(), *key); + assert_eq!(TransactionKeyManagerBranch::from_key(key), *branch); + }, + TransactionKeyManagerBranch::Nonce => { + assert_eq!(branch.as_byte(), *expected_byte); + assert_eq!(TransactionKeyManagerBranch::from_byte(*expected_byte), Some(*branch)); + assert_eq!(&branch.get_branch_key(), *key); + assert_eq!(TransactionKeyManagerBranch::from_key(key), *branch); + }, + TransactionKeyManagerBranch::KernelNonce => { + assert_eq!(branch.as_byte(), *expected_byte); + assert_eq!(TransactionKeyManagerBranch::from_byte(*expected_byte), Some(*branch)); + assert_eq!(&branch.get_branch_key(), *key); + assert_eq!(TransactionKeyManagerBranch::from_key(key), *branch); + }, + TransactionKeyManagerBranch::SenderOffset => { + assert_eq!(branch.as_byte(), *expected_byte); + assert_eq!(TransactionKeyManagerBranch::from_byte(*expected_byte), Some(*branch)); + assert_eq!(&branch.get_branch_key(), *key); + assert_eq!(TransactionKeyManagerBranch::from_key(key), *branch); + }, + TransactionKeyManagerBranch::SenderOffsetLedger => { + assert_eq!(branch.as_byte(), *expected_byte); + assert_eq!(TransactionKeyManagerBranch::from_byte(*expected_byte), Some(*branch)); + assert_eq!(&branch.get_branch_key(), *key); + assert_eq!(TransactionKeyManagerBranch::from_key(key), *branch); + }, + TransactionKeyManagerBranch::Spend => { + assert_eq!(branch.as_byte(), *expected_byte); + assert_eq!(TransactionKeyManagerBranch::from_byte(*expected_byte), Some(*branch)); + assert_eq!(&branch.get_branch_key(), *key); + assert_eq!(TransactionKeyManagerBranch::from_key(key), *branch); + }, + TransactionKeyManagerBranch::RandomKey => { + assert_eq!(branch.as_byte(), *expected_byte); + assert_eq!(TransactionKeyManagerBranch::from_byte(*expected_byte), Some(*branch)); + assert_eq!(&branch.get_branch_key(), *key); + assert_eq!(TransactionKeyManagerBranch::from_key(key), *branch); + }, + TransactionKeyManagerBranch::PreMine => { + assert_eq!(branch.as_byte(), *expected_byte); + assert_eq!(TransactionKeyManagerBranch::from_byte(*expected_byte), Some(*branch)); + assert_eq!(&branch.get_branch_key(), *key); + assert_eq!(TransactionKeyManagerBranch::from_key(key), *branch); + }, + } + } + } } diff --git a/base_layer/common_types/src/wallet_types.rs b/base_layer/common_types/src/wallet_types.rs index 3da235a4e9..d30d5d69d2 100644 --- a/base_layer/common_types/src/wallet_types.rs +++ b/base_layer/common_types/src/wallet_types.rs @@ -31,7 +31,7 @@ use tari_crypto::keys::PublicKey as PublicKeyTrait; use crate::types::{PrivateKey, PublicKey}; -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] pub enum WalletType { #[default] DerivedKeys, @@ -49,7 +49,7 @@ impl Display for WalletType { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct ProvidedKeysWallet { pub public_spend_key: PublicKey, pub private_spend_key: Option, @@ -64,7 +64,7 @@ impl Display for ProvidedKeysWallet { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct LedgerWallet { pub account: u64, pub public_alpha: Option, @@ -74,8 +74,10 @@ pub struct LedgerWallet { impl Display for LedgerWallet { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "account {}", self.account)?; - write!(f, "pubkey {}", self.public_alpha.is_some())?; + write!(f, "account '{}', ", self.account)?; + write!(f, "network '{}', ", self.network)?; + write!(f, "public_alpha '{}', ", self.public_alpha.is_some())?; + write!(f, "view_key '{}'", self.view_key.is_some())?; Ok(()) } } diff --git a/base_layer/core/Cargo.toml b/base_layer/core/Cargo.toml index 7629e76b0c..94db7c57b1 100644 --- a/base_layer/core/Cargo.toml +++ b/base_layer/core/Cargo.toml @@ -93,6 +93,7 @@ primitive-types = { version = "0.12", features = ["serde"] } tiny-keccak = { package = "tari-tiny-keccak", version = "2.0.2", features = [ "keccak", ] } +dirs-next = "1.0.2" [dev-dependencies] criterion = { version = "0.4.0" } diff --git a/base_layer/core/src/blocks/mod.rs b/base_layer/core/src/blocks/mod.rs index 365fd2bcb9..d301cc7c39 100644 --- a/base_layer/core/src/blocks/mod.rs +++ b/base_layer/core/src/blocks/mod.rs @@ -48,7 +48,7 @@ pub use block_header::{BlockHeader, BlockHeaderValidationError}; pub mod genesis_block; #[cfg(feature = "base_node")] -mod pre_mine; +pub mod pre_mine; #[cfg(feature = "base_node")] mod historical_block; diff --git a/base_layer/core/src/blocks/pre_mine/mod.rs b/base_layer/core/src/blocks/pre_mine/mod.rs index ddeaae853f..c46d0dbab0 100644 --- a/base_layer/core/src/blocks/pre_mine/mod.rs +++ b/base_layer/core/src/blocks/pre_mine/mod.rs @@ -19,143 +19,357 @@ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#[cfg(test)] -mod test { - use std::{ - convert::{TryFrom, TryInto}, - fs::File, - io::Write, - }; - use rand::{rngs::OsRng, seq::SliceRandom, thread_rng}; - use tari_common_types::{ - key_branches::TransactionKeyManagerBranch, - tari_address::TariAddress, - types::{Commitment, PrivateKey, PublicKey, Signature}, - }; - use tari_crypto::keys::{PublicKey as PkTrait, SecretKey as SkTrait}; - use tari_key_manager::key_manager_service::KeyManagerInterface; - use tari_script::{script, ExecutionStack}; - use tari_utilities::ByteArray; +use std::convert::{TryFrom, TryInto}; - use crate::{ - one_sided::public_key_to_output_encryption_key, - transactions::{ - key_manager::{ - create_memory_db_key_manager, - SecretTransactionKeyManagerInterface, - TransactionKeyManagerInterface, - }, - tari_amount::MicroMinotari, - transaction_components::{ - encrypted_data::PaymentId, - KernelFeatures, - OutputFeatures, - OutputFeaturesVersion, - OutputType, - RangeProofType, - TransactionKernel, - TransactionKernelVersion, - TransactionOutput, - TransactionOutputVersion, - WalletOutputBuilder, - }, - transaction_protocol::TransactionMetadata, +use rand::{prelude::SliceRandom, rngs::OsRng, thread_rng}; +use tari_common_types::{ + key_branches::TransactionKeyManagerBranch, + types::{Commitment, PrivateKey, PublicKey, Signature}, +}; +use tari_crypto::keys::{PublicKey as PkTrait, SecretKey as SkTrait}; +use tari_key_manager::key_manager_service::KeyManagerInterface; +use tari_script::{script, ExecutionStack}; +use tari_utilities::ByteArray; + +use crate::{ + one_sided::public_key_to_output_encryption_key, + transactions::{ + key_manager::{ + create_memory_db_key_manager, + SecretTransactionKeyManagerInterface, + TransactionKeyManagerInterface, }, - }; + tari_amount::MicroMinotari, + transaction_components::{ + encrypted_data::PaymentId, + KernelFeatures, + OutputFeatures, + OutputFeaturesVersion, + OutputType, + RangeProofType, + TransactionKernel, + TransactionKernelVersion, + TransactionOutput, + TransactionOutputVersion, + WalletOutputBuilder, + }, + transaction_protocol::TransactionMetadata, + }, +}; - pub async fn create_pre_mine( - amount: MicroMinotari, - num_utxos: usize, - signature_threshold: u8, - start_lock_height: u64, - lock_height_increase: u64, - addresses: Vec, - backup_address: TariAddress, - fail_safe_height: u64, - ) -> (Vec, TransactionKernel) { - let mut list_of_spend_keys = Vec::new(); - let mut total_script_key = PublicKey::default(); - let key_manager = create_memory_db_key_manager().unwrap(); - for address in &addresses { - list_of_spend_keys.push(address.public_spend_key().clone()); - total_script_key = total_script_key + address.public_spend_key(); +/// Token unlock schedule +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct UnlockSchedule { + /// Network rewards + pub network_rewards: Apportionment, + /// Protocol tokens + pub protocol: Apportionment, + /// Community tokens + pub community: Apportionment, + /// Contributors' tokens + pub contributors: Apportionment, + /// Participants' tokens + pub participants: Apportionment, +} + +/// Token apportionment +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Apportionment { + /// Beneficiary of the apportionment + pub beneficiary: String, + /// Percentage of total tokens + pub percentage: u64, + /// Total tokens for this apportionment + pub tokens_amount: u64, + /// Token release cadence schedule + pub schedule: Option, +} + +/// Token release cadence +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ReleaseCadence { + /// Initial lockup days + pub initial_lockup_days: u64, + /// Monthly fraction release factor + pub monthly_fraction_denominator: u64, + /// Upfront release percentage + pub upfront_release: Option, +} + +/// The upfront percentage of the total tokens to be released +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct UpfrontRelease { + /// The fraction of the total tokens to be released upfront + pub percentage: u64, + /// The number of tokens it has to be divided into + pub number_of_tokens: u64, +} + +/// Get the tokenomics unlock schedule as per the specification - see `https://tari.substack.com/p/tari-tokenomics` +pub fn get_tokenomics_pre_mine_unlock_schedule() -> UnlockSchedule { + UnlockSchedule { + network_rewards: Apportionment { + beneficiary: "network_rewards".to_string(), + percentage: 70, + tokens_amount: 14_700_000_000, + schedule: None, + }, + protocol: Apportionment { + beneficiary: "protocol".to_string(), + percentage: 9, + tokens_amount: 1_890_000_000, + schedule: Some(ReleaseCadence { + initial_lockup_days: 180, + monthly_fraction_denominator: 48, + upfront_release: Some(UpfrontRelease { + percentage: 40, + number_of_tokens: 20, + }), + }), + }, + community: Apportionment { + beneficiary: "community".to_string(), + percentage: 5, + tokens_amount: 1_050_000_000, + schedule: Some(ReleaseCadence { + initial_lockup_days: 180, + monthly_fraction_denominator: 12, + upfront_release: None, + }), + }, + contributors: Apportionment { + beneficiary: "contributors".to_string(), + percentage: 4, + tokens_amount: 840_000_000, + schedule: Some(ReleaseCadence { + initial_lockup_days: 365, + monthly_fraction_denominator: 60, + upfront_release: None, + }), + }, + participants: Apportionment { + beneficiary: "participants".to_string(), + percentage: 12, + tokens_amount: 2_520_000_000, + schedule: Some(ReleaseCadence { + initial_lockup_days: 365, + monthly_fraction_denominator: 24, + upfront_release: None, + }), + }, + } +} + +/// Pre-mine values +#[derive(Debug)] +pub struct PreMineItem { + pub value: MicroMinotari, + pub maturity: u64, + pub beneficiary: String, +} + +/// Create a list of (token value, maturity in blocks) according to the amounts in the unlock schedule, based on the +/// apportionment and release cadence where 1 day equals 24 * 60 / 2 blocks. +pub fn create_pre_mine_output_values(schedule: UnlockSchedule) -> Result, String> { + let mut values_with_maturity = Vec::new(); + let blocks_per_day = 24 * 60 / 2; + let days_per_month = 365.25 / 12f64; + #[allow(clippy::cast_possible_truncation)] + let blocks_per_month = (days_per_month * blocks_per_day as f64) as u64; + for apportionment in &[ + &schedule.network_rewards, + &schedule.protocol, + &schedule.community, + &schedule.contributors, + &schedule.participants, + ] { + if let Some(schedule) = apportionment.schedule.as_ref() { + let upfront_release = schedule.upfront_release.clone().unwrap_or_default(); + if upfront_release.percentage > 100 { + return Err(format!( + "Upfront percentage must be less than or equal to 100 in {:?}", + apportionment + )); + } + if apportionment + .tokens_amount + .checked_mul(1_000_000 * upfront_release.percentage) + .is_none() + { + return Err(format!("Minotari calculation overflow in {:?}", apportionment)); + } + let mut tokens_value = apportionment.tokens_amount * 1_000_000; + if upfront_release.percentage > 0 { + let upfront_tokens = tokens_value * upfront_release.percentage / 100; + tokens_value -= upfront_tokens; + let value_per_round = upfront_tokens / upfront_release.number_of_tokens; + let mut assigned_tokens = 0; + for _ in 0..upfront_release.number_of_tokens - 1 { + values_with_maturity.push(PreMineItem { + value: MicroMinotari::from(value_per_round), + maturity: 0, + beneficiary: apportionment.beneficiary.clone(), + }); + assigned_tokens += value_per_round; + } + values_with_maturity.push(PreMineItem { + value: MicroMinotari::from(upfront_tokens - assigned_tokens), + maturity: 0, + beneficiary: apportionment.beneficiary.clone(), + }); + } + let monthly_tokens = tokens_value / schedule.monthly_fraction_denominator; + let mut total_tokens = 0; + let mut maturity = 0; + for i in 0..schedule.monthly_fraction_denominator - 1 { + total_tokens += monthly_tokens; + maturity = schedule.initial_lockup_days * blocks_per_day + i * blocks_per_month; + values_with_maturity.push(PreMineItem { + value: MicroMinotari::from(monthly_tokens), + maturity, + beneficiary: apportionment.beneficiary.clone(), + }); + } + let last_tokens = tokens_value - total_tokens; + values_with_maturity.push(PreMineItem { + value: MicroMinotari::from(last_tokens), + maturity: maturity + blocks_per_month, + beneficiary: apportionment.beneficiary.clone(), + }); } + } + Ok(values_with_maturity) +} + +/// Get the pre-mine items according to the pre-mine specification +pub async fn get_pre_mine_items() -> Result, String> { + let schedule = get_tokenomics_pre_mine_unlock_schedule(); + create_pre_mine_output_values(schedule) +} + +// The threshold is 1 more than half of the public keys if even, otherwise 1 more than half of 'public keys - 1' +fn get_signature_threshold(number_of_keys: usize) -> Result { + if number_of_keys < 2 { + return Err("Invalid number of parties, must be > 1".to_string()); + } + u8::try_from(number_of_keys / 2 + 1).map_err(|e| e.to_string()) +} + +/// Create a pre-mine genesis block file with the given pre-mine items and party public keys +pub async fn create_pre_mine_genesis_block_file( + pre_mine_items: &[PreMineItem], + threshold_spend_keys: &[Vec], + backup_spend_keys: &[PublicKey], +) -> Result<(Vec, TransactionKernel), String> { + // 1 month fail-safe height for each pre-mine output at which time the backup_address can spend the output + let fail_safe_height = 720 * 30; + let mut outputs = Vec::new(); + let mut total_private_key = PrivateKey::default(); + for (i, ((item, public_keys), backup_key)) in pre_mine_items + .iter() + .zip(threshold_spend_keys) + .zip(backup_spend_keys) + .enumerate() + { + let signature_threshold = get_signature_threshold(public_keys.len())?; + let total_script_key = public_keys.iter().fold(PublicKey::default(), |acc, x| acc + x); + let key_manager = create_memory_db_key_manager().unwrap(); let view_key = public_key_to_output_encryption_key(&total_script_key).unwrap(); let view_key_id = key_manager.import_key(view_key.clone()).await.unwrap(); - let address_len = u8::try_from(addresses.len()).unwrap(); - let mut outputs = Vec::new(); - let mut total_private_key = PrivateKey::default(); - let mut lock_height = start_lock_height; - - for i in 0..num_utxos { - let (commitment_mask, script_key) = key_manager.get_next_commitment_mask_and_script_key().await.unwrap(); - total_private_key = - total_private_key + &key_manager.get_private_key(&commitment_mask.key_id).await.unwrap(); - let commitment = key_manager - .get_commitment(&commitment_mask.key_id, &amount.into()) - .await - .unwrap(); - let mut commitment_bytes = [0u8; 32]; - commitment_bytes.clone_from_slice(commitment.as_bytes()); + let address_len = u8::try_from(public_keys.len()).unwrap(); - let sender_offset = key_manager - .get_next_key(TransactionKeyManagerBranch::SenderOffset.get_branch_key()) - .await - .unwrap(); - list_of_spend_keys.shuffle(&mut thread_rng()); - let script = script!( - CheckHeight(lock_height + fail_safe_height) LeZero IfThen CheckMultiSigVerifyAggregatePubKey(signature_threshold,address_len,list_of_spend_keys.clone(),Box::new(commitment_bytes)) Else PushPubKey(Box::new(backup_address.public_spend_key().clone())) EndIf - ); - let output = WalletOutputBuilder::new(amount, commitment_mask.key_id) - .with_features(OutputFeatures::new( - OutputFeaturesVersion::get_current_version(), - OutputType::Standard, - lock_height, - Vec::new(), - None, - RangeProofType::RevealedValue, - )) - .with_script(script) - .encrypt_data_for_recovery(&key_manager, Some(&view_key_id), PaymentId::U64(i.try_into().unwrap())) - .await - .unwrap() - .with_input_data(ExecutionStack::default()) - .with_version(TransactionOutputVersion::get_current_version()) - .with_sender_offset_public_key(sender_offset.pub_key) - .with_script_key(script_key.key_id) - .with_minimum_value_promise(amount) - .sign_as_sender_and_receiver(&key_manager, &sender_offset.key_id) - .await - .unwrap() - .try_build(&key_manager) - .await - .unwrap(); - outputs.push(output.to_transaction_output(&key_manager).await.unwrap()); - lock_height += lock_height_increase; - } - // lets create a single kernel for all the outputs - let r = PrivateKey::random(&mut OsRng); - let tx_meta = TransactionMetadata::new_with_features(0.into(), 0, KernelFeatures::empty()); - let total_public_key = PublicKey::from_secret_key(&total_private_key); - let e = TransactionKernel::build_kernel_challenge_from_tx_meta( - &TransactionKernelVersion::get_current_version(), - &PublicKey::from_secret_key(&r), - &total_public_key, - &tx_meta, + let (commitment_mask, script_key) = key_manager.get_next_commitment_mask_and_script_key().await.unwrap(); + total_private_key = total_private_key + &key_manager.get_private_key(&commitment_mask.key_id).await.unwrap(); + let commitment = key_manager + .get_commitment(&commitment_mask.key_id, &item.value.into()) + .await + .unwrap(); + let mut commitment_bytes = [0u8; 32]; + commitment_bytes.clone_from_slice(commitment.as_bytes()); + + let sender_offset = key_manager + .get_next_key(TransactionKeyManagerBranch::SenderOffset.get_branch_key()) + .await + .unwrap(); + let mut public_keys = public_keys.clone(); + public_keys.shuffle(&mut thread_rng()); + let script = script!( + CheckHeight(item.maturity + fail_safe_height) LeZero + IfThen + CheckMultiSigVerifyAggregatePubKey(signature_threshold, address_len, public_keys.clone(), Box::new(commitment_bytes)) + Else + PushPubKey(Box::new(backup_key.clone())) + EndIf ); - let signature = Signature::sign_raw_uniform(&total_private_key, r, &e).unwrap(); - let excess = Commitment::from_public_key(&total_public_key); - let kernel = - TransactionKernel::new_current_version(KernelFeatures::empty(), 0.into(), 0, excess, signature, None); - (outputs, kernel) + let output = WalletOutputBuilder::new(item.value, commitment_mask.key_id) + .with_features(OutputFeatures::new( + OutputFeaturesVersion::get_current_version(), + OutputType::Standard, + item.maturity, + Vec::new(), + None, + RangeProofType::RevealedValue, + )) + .with_script(script) + .encrypt_data_for_recovery(&key_manager, Some(&view_key_id), PaymentId::U64(i.try_into().unwrap())) + .await + .unwrap() + .with_input_data(ExecutionStack::default()) + .with_version(TransactionOutputVersion::get_current_version()) + .with_sender_offset_public_key(sender_offset.pub_key) + .with_script_key(script_key.key_id) + .with_minimum_value_promise(item.value) + .sign_as_sender_and_receiver(&key_manager, &sender_offset.key_id) + .await + .unwrap() + .try_build(&key_manager) + .await + .unwrap(); + outputs.push(output.to_transaction_output(&key_manager).await.unwrap()); } + // lets create a single kernel for all the outputs + let r = PrivateKey::random(&mut OsRng); + let tx_meta = TransactionMetadata::new_with_features(0.into(), 0, KernelFeatures::empty()); + let total_public_key = PublicKey::from_secret_key(&total_private_key); + let e = TransactionKernel::build_kernel_challenge_from_tx_meta( + &TransactionKernelVersion::get_current_version(), + &PublicKey::from_secret_key(&r), + &total_public_key, + &tx_meta, + ); + let signature = Signature::sign_raw_uniform(&total_private_key, r, &e).unwrap(); + let excess = Commitment::from_public_key(&total_public_key); + let kernel = TransactionKernel::new_current_version(KernelFeatures::empty(), 0.into(), 0, excess, signature, None); + Ok((outputs, kernel)) +} + +#[cfg(test)] +mod test { + use std::{fs, fs::File, io::Write}; + + use tari_common_types::tari_address::TariAddress; + + use crate::{ + blocks::pre_mine::{ + create_pre_mine_genesis_block_file, + create_pre_mine_output_values, + get_signature_threshold, + get_tokenomics_pre_mine_unlock_schedule, + Apportionment, + PreMineItem, + ReleaseCadence, + UpfrontRelease, + }, + transactions::tari_amount::MicroMinotari, + }; // Only run this when you want to create a new utxo file #[ignore] #[tokio::test] async fn print_pre_mine() { - let addresses = vec![ + let addresses_for_round = vec![ + // This wil be public keys TariAddress::from_base58( "f4bYsv3sEMroDGKMMjhgm7cp1jDShdRWQzmV8wZiD6sJPpAEuezkiHtVhn7akK3YqswH5t3sUASW7rbvPSqMBDSCSp", ) @@ -169,26 +383,45 @@ mod test { ) .unwrap(), ]; - for address in &addresses { - println!("{}", address.public_spend_key()); - } let backup_address = TariAddress::from_base58( "f4GYN3QVRboH6uwG9oFj3LjmUd4XVd1VDYiT6rNd4gCpZF6pY7iuoCpoajfDfuPynS7kspXU5hKRMWLTP9CRjoe1hZU", ) .unwrap(); - // lets create a pre_mine with 10 outputs of 1000T each - let (outputs, kernel) = create_pre_mine( - MicroMinotari::from(2_000_000_000), - 100, - 2, - 5, - 5, - addresses, - backup_address, - 200, - ) - .await; - let mut utxo_file = File::create("utxos.json").expect("Could not create utxos.json"); + let public_spend_keys_for_round: Vec<_> = addresses_for_round + .iter() + .map(|address| address.public_spend_key().clone()) + .collect(); + + let schedule = get_tokenomics_pre_mine_unlock_schedule(); + let mut pre_mine_items = create_pre_mine_output_values(schedule.clone()).unwrap(); + // Add some test outputs + for _ in 0..20 { + pre_mine_items.push(PreMineItem { + value: MicroMinotari::from(1_000_000), + maturity: 0, + beneficiary: "test_output".to_string(), + }); + } + let mut public_spend_keys = Vec::with_capacity(pre_mine_items.len()); + let mut backup_public_spend_keys = Vec::with_capacity(pre_mine_items.len()); + for _ in 0..pre_mine_items.len() { + public_spend_keys.push(public_spend_keys_for_round.clone()); + backup_public_spend_keys.push(backup_address.public_spend_key().clone()); + } + + let (outputs, kernel) = + create_pre_mine_genesis_block_file(&pre_mine_items, &public_spend_keys, &backup_public_spend_keys) + .await + .unwrap(); + + let base_dir = dirs_next::document_dir().unwrap(); + let file_path = base_dir.join("tari_pre_mine").join("create").join("utxos.json"); + if let Some(path) = file_path.parent() { + if !path.exists() { + fs::create_dir_all(path).unwrap(); + } + } + let mut utxo_file = File::create(&file_path).expect("Could not create 'utxos.json'"); for output in outputs { let utxo_s = serde_json::to_string(&output).unwrap(); @@ -197,5 +430,181 @@ mod test { let kernel = serde_json::to_string(&kernel).unwrap(); let _result = utxo_file.write_all(format!("{}\n", kernel).as_bytes()); + println!( + "\nOutputs written to: '{}'\n", + fs::canonicalize(&file_path).unwrap().display() + ); + } + + #[test] + fn test_get_tokenomics_pre_mine_unlock_schedule() { + let schedule = get_tokenomics_pre_mine_unlock_schedule(); + assert_eq!(schedule.network_rewards, Apportionment { + beneficiary: "network_rewards".to_string(), + percentage: 70, + tokens_amount: 14_700_000_000, + schedule: None, + }); + assert_eq!(schedule.protocol, Apportionment { + beneficiary: "protocol".to_string(), + percentage: 9, + tokens_amount: 1_890_000_000, + schedule: Some(ReleaseCadence { + initial_lockup_days: 180, + monthly_fraction_denominator: 48, + upfront_release: Some(UpfrontRelease { + percentage: 40, + number_of_tokens: 20 + }), + }), + }); + assert_eq!( + schedule.protocol.tokens_amount * schedule.protocol.schedule.unwrap().upfront_release.unwrap().percentage / + 100, + 756_000_000 + ); + assert_eq!(schedule.community, Apportionment { + beneficiary: "community".to_string(), + percentage: 5, + tokens_amount: 1_050_000_000, + schedule: Some(ReleaseCadence { + initial_lockup_days: 180, + monthly_fraction_denominator: 12, + upfront_release: None, + }), + }); + assert_eq!(schedule.contributors, Apportionment { + beneficiary: "contributors".to_string(), + percentage: 4, + tokens_amount: 840_000_000, + schedule: Some(ReleaseCadence { + initial_lockup_days: 365, + monthly_fraction_denominator: 60, + upfront_release: None, + }), + }); + assert_eq!(schedule.participants, Apportionment { + beneficiary: "participants".to_string(), + percentage: 12, + tokens_amount: 2_520_000_000, + schedule: Some(ReleaseCadence { + initial_lockup_days: 365, + monthly_fraction_denominator: 24, + upfront_release: None, + }), + }); + + assert_eq!( + schedule.participants.percentage + + schedule.contributors.percentage + + schedule.community.percentage + + schedule.protocol.percentage + + schedule.network_rewards.percentage, + 100 + ); + + assert_eq!( + schedule.participants.tokens_amount + + schedule.contributors.tokens_amount + + schedule.community.tokens_amount + + schedule.protocol.tokens_amount + + schedule.network_rewards.tokens_amount, + 21_000_000_000 + ); + } + + #[test] + fn test_create_pre_mine_output_values() { + let schedule = get_tokenomics_pre_mine_unlock_schedule(); + let pre_mine_items = create_pre_mine_output_values(schedule.clone()).unwrap(); + for item in &pre_mine_items { + println!("{:?}", item); + } + + // Verify pre_mine items as per `https://tari.substack.com/p/tari-tokenomics` + let total_pre_mine_value = pre_mine_items.iter().map(|item| item.value).sum::(); + let total_tokens = schedule.network_rewards.tokens_amount + + schedule.protocol.tokens_amount + + schedule.community.tokens_amount + + schedule.contributors.tokens_amount + + schedule.participants.tokens_amount; + let total_value = MicroMinotari::from(total_tokens * 1_000_000); + assert_eq!( + total_pre_mine_value + MicroMinotari::from(schedule.network_rewards.tokens_amount * 1_000_000), + total_value + ); + let protocol_tokens = pre_mine_items + .iter() + .filter(|item| item.beneficiary == "protocol") + .map(|item| item.value) + .sum::(); + assert_eq!( + protocol_tokens, + MicroMinotari::from(schedule.protocol.tokens_amount * 1_000_000) + ); + let protocol_tokens_at_start = pre_mine_items + .iter() + .filter(|item| item.beneficiary == "protocol" && item.maturity == 0) + .map(|item| item.value) + .sum::(); + assert_eq!(protocol_tokens_at_start, MicroMinotari::from(756_000_000 * 1_000_000)); + let all_tokens_at_start = pre_mine_items + .iter() + .filter(|item| item.maturity == 0) + .map(|item| item.value) + .sum::(); + assert_eq!(all_tokens_at_start, MicroMinotari::from(756_000_000 * 1_000_000)); + let community_tokens = pre_mine_items + .iter() + .filter(|item| item.beneficiary == "community") + .map(|item| item.value) + .sum::(); + assert_eq!( + community_tokens, + MicroMinotari::from(schedule.community.tokens_amount * 1_000_000) + ); + let contributors_tokens = pre_mine_items + .iter() + .filter(|item| item.beneficiary == "contributors") + .map(|item| item.value) + .sum::(); + assert_eq!( + contributors_tokens, + MicroMinotari::from(schedule.contributors.tokens_amount * 1_000_000) + ); + let participants_tokens = pre_mine_items + .iter() + .filter(|item| item.beneficiary == "participants") + .map(|item| item.value) + .sum::(); + assert_eq!( + participants_tokens, + MicroMinotari::from(schedule.participants.tokens_amount * 1_000_000) + ); + } + + #[test] + fn test_get_signature_threshold() { + assert!(get_signature_threshold(0).is_err()); + assert!(get_signature_threshold(1).is_err()); + assert_eq!(get_signature_threshold(2).unwrap(), 2); + assert_eq!(get_signature_threshold(3).unwrap(), 2); + assert_eq!(get_signature_threshold(4).unwrap(), 3); + assert_eq!(get_signature_threshold(5).unwrap(), 3); + assert_eq!(get_signature_threshold(6).unwrap(), 4); + assert_eq!(get_signature_threshold(7).unwrap(), 4); + assert_eq!(get_signature_threshold(8).unwrap(), 5); + assert_eq!(get_signature_threshold(9).unwrap(), 5); + assert_eq!(get_signature_threshold(10).unwrap(), 6); + assert_eq!(get_signature_threshold(11).unwrap(), 6); + assert_eq!(get_signature_threshold(12).unwrap(), 7); + assert_eq!(get_signature_threshold(13).unwrap(), 7); + assert_eq!(get_signature_threshold(14).unwrap(), 8); + assert_eq!(get_signature_threshold(15).unwrap(), 8); + assert_eq!(get_signature_threshold(16).unwrap(), 9); + assert_eq!(get_signature_threshold(17).unwrap(), 9); + assert_eq!(get_signature_threshold(18).unwrap(), 10); + assert_eq!(get_signature_threshold(19).unwrap(), 10); + assert_eq!(get_signature_threshold(20).unwrap(), 11); } } diff --git a/base_layer/core/src/transactions/key_manager/inner.rs b/base_layer/core/src/transactions/key_manager/inner.rs index 71b6aebe47..08eb5c5049 100644 --- a/base_layer/core/src/transactions/key_manager/inner.rs +++ b/base_layer/core/src/transactions/key_manager/inner.rs @@ -188,10 +188,11 @@ where TBackend: KeyManagerBackend + 'static pub async fn get_random_key(&self) -> Result, KeyManagerServiceError> { match &self.wallet_type { WalletType::Ledger(ledger) => { + debug!(target: LOG_TARGET, "get_random_key: wallet type {}", self.wallet_type); #[cfg(not(feature = "ledger"))] { Err(KeyManagerServiceError::LedgerError(format!( - "Ledger {} is not supported", + "Ledger {} is not supported in this build, please enable the \"ledger\" feature for core", ledger ))) } @@ -242,7 +243,9 @@ where TBackend: KeyManagerBackend + 'static #[cfg(not(feature = "ledger"))] { Err(KeyManagerServiceError::LedgerError( - "Ledger is not supported".to_string(), + "Ledger is not supported in this build, please enable the \"ledger\" feature for \ + core" + .to_string(), )) } @@ -416,6 +419,10 @@ where TBackend: KeyManagerBackend + 'static } } + pub fn get_wallet_type(&self) -> WalletType { + self.wallet_type.clone() + } + pub async fn get_view_key(&self) -> Result, KeyManagerServiceError> { let key_id = KeyId::Managed { branch: TransactionKeyManagerBranch::DataEncryption.get_branch_key(), @@ -660,10 +667,12 @@ where TBackend: KeyManagerBackend + 'static if let KeyId::Managed { branch, index } = secret_key_id { match TransactionKeyManagerBranch::from_key(branch) { TransactionKeyManagerBranch::OneSidedSenderOffset | TransactionKeyManagerBranch::RandomKey => { + debug!(target: LOG_TARGET, "get_diffie_hellman_shared_secret: wallet type {}", self.wallet_type); #[cfg(not(feature = "ledger"))] { return Err(TransactionError::LedgerNotSupported(format!( - "Ledger {} (has index {}) is not supported", + "Ledger {} (has index {}) is not supported in this build, please enable the \ + \"ledger\" feature for core", ledger, index ))); } @@ -698,10 +707,12 @@ where TBackend: KeyManagerBackend + 'static WalletType::Ledger(ledger) => match secret_key_id { KeyId::Managed { branch, index } => match TransactionKeyManagerBranch::from_key(branch) { TransactionKeyManagerBranch::OneSidedSenderOffset => { + debug!(target: LOG_TARGET, "get_diffie_hellman_stealth_domain_hasher: allet type {}", self.wallet_type); #[cfg(not(feature = "ledger"))] { Err(TransactionError::LedgerNotSupported(format!( - "Ledger {} (has index {}) is not supported", + "Ledger {} (has index {}) is not supported in this build, please enable the \ + \"ledger\" feature for core", ledger, index ))) } @@ -781,11 +792,13 @@ where TBackend: KeyManagerBackend + 'static match (&self.wallet_type, script_key_id) { (WalletType::Ledger(ledger), KeyId::Derived { key }) => { + debug!(target: LOG_TARGET, "get_script_signature: wallet type {}", self.wallet_type); #[cfg(not(feature = "ledger"))] { Err(TransactionError::LedgerNotSupported(format!( - "Ledger {} (has script_key_id {}) with key {} is not supported", - ledger, script_key_id, key, + "Ledger {} (has script_key_id {}, branch {}, index {}) is not supported in this build, please \ + enable the \"ledger\" feature for core", + ledger, script_key_id, branch, index ))) } @@ -980,10 +993,11 @@ where TBackend: KeyManagerBackend + 'static Ok(script_offset) }, WalletType::Ledger(ledger) => { + debug!(target: LOG_TARGET, "get_script_offset: wallet type {}", self.wallet_type); #[cfg(not(feature = "ledger"))] { Err(TransactionError::LedgerNotSupported(format!( - "Ledger {} is not supported", + "Ledger {} is not supported in this build, please enable the \"ledger\" feature for core", ledger ))) } @@ -1068,10 +1082,11 @@ where TBackend: KeyManagerBackend + 'static ) -> Result { match &self.wallet_type { WalletType::Ledger(ledger) => { + debug!(target: LOG_TARGET, "sign_script_message: wallet type {}", self.wallet_type); #[cfg(not(feature = "ledger"))] { Err(TransactionError::LedgerNotSupported(format!( - "Ledger {} is not supported", + "Ledger {} is not supported in this build, please enable the \"ledger\" feature for core", ledger ))) } @@ -1112,10 +1127,11 @@ where TBackend: KeyManagerBackend + 'static ) -> Result { match &self.wallet_type { WalletType::Ledger(ledger) => { + debug!(target: LOG_TARGET, "sign_with_nonce_and_challenge: wallet type {}", self.wallet_type); #[cfg(not(feature = "ledger"))] { Err(TransactionError::LedgerNotSupported(format!( - "Ledger {} is not supported", + "Ledger {} is not supported in this build, please enable the \"ledger\" feature for core", ledger ))) } diff --git a/base_layer/core/src/transactions/key_manager/wrapper.rs b/base_layer/core/src/transactions/key_manager/wrapper.rs index 727411be82..71c092c0b2 100644 --- a/base_layer/core/src/transactions/key_manager/wrapper.rs +++ b/base_layer/core/src/transactions/key_manager/wrapper.rs @@ -96,6 +96,11 @@ where TBackend: KeyManagerBackend + 'static )?)), }) } + + /// Get the wallet type + pub async fn get_wallet_type(&self) -> WalletType { + self.transaction_key_manager_inner.read().await.get_wallet_type() + } } #[async_trait::async_trait] From e9bd7cc6b8dec8f5ebceb86b86993075fe25390b Mon Sep 17 00:00:00 2001 From: Hansie Odendaal Date: Wed, 31 Jul 2024 14:48:01 +0200 Subject: [PATCH 2/3] testing spend --- .../src/automation/commands.rs | 53 +++- .../src/automation/mod.rs | 1 - .../comms/src/accessor_methods.rs | 5 +- .../wallet_output_builder.rs | 10 +- .../src/output_manager_service/service.rs | 116 ++++++-- .../wallet/src/transaction_service/handle.rs | 49 ---- .../wallet/src/transaction_service/service.rs | 259 +----------------- 7 files changed, 152 insertions(+), 341 deletions(-) diff --git a/applications/minotari_console_wallet/src/automation/commands.rs b/applications/minotari_console_wallet/src/automation/commands.rs index b417509cfd..94cdb0ea0a 100644 --- a/applications/minotari_console_wallet/src/automation/commands.rs +++ b/applications/minotari_console_wallet/src/automation/commands.rs @@ -238,6 +238,8 @@ async fn finalise_aggregate_utxo( script_signatures: Vec, wallet_script_secret_key: PrivateKey, ) -> Result { + trace!(target: LOG_TARGET, "finalise_aggregate_utxo: start"); + let mut meta_sig = Signature::default(); for sig in &meta_signatures { meta_sig = &meta_sig + sig; @@ -246,6 +248,7 @@ async fn finalise_aggregate_utxo( for sig in &script_signatures { script_sig = &script_sig + sig; } + trace!(target: LOG_TARGET, "finalise_aggregate_utxo: aggregated signatures"); wallet_transaction_service .finalize_aggregate_utxo(tx_id, meta_sig, script_sig, wallet_script_secret_key) @@ -824,7 +827,7 @@ pub async fn command_runner( break; }, }; - let signature = match key_manager_service + let verification_signature = match key_manager_service .sign_script_message(&key_id, PrivateKey::from(index).as_bytes()) .await { @@ -838,7 +841,7 @@ pub async fn command_runner( outputs_for_leader.push(PreMineCreateStep1ForLeader { index, script_public_key, - verification_signature: signature, + verification_signature, }); } if error { @@ -1203,6 +1206,14 @@ pub async fn command_runner( println!(); }, PreMineSpendSessionInfo(args) => { + match key_manager_service.get_wallet_type().await { + WalletType::Ledger(_) => {}, + _ => { + eprintln!("\nError: Wallet type must be 'Ledger' to spend pre-mine outputs!\n"); + break; + }, + } + let embedded_output = match get_embedded_pre_mine_outputs(vec![args.output_index]) { Ok(outputs) => outputs[0].clone(), Err(e) => { @@ -1288,6 +1299,14 @@ pub async fn command_runner( }, PreMineCreatePartyDetails(args) => { PreMineSpendPartyDetails(args) => { + match key_manager_service.get_wallet_type().await { + WalletType::Ledger(_) => {}, + _ => { + eprintln!("\nError: Wallet type must be 'Ledger' to spend pre-mine outputs!\n"); + break; + }, + } + if args.alias.is_empty() || args.alias.contains(" ") { eprintln!("\nError: Alias cannot contain spaces!\n"); break; @@ -1361,15 +1380,13 @@ pub async fn command_runner( break; }, }; - let script_input_signature = key_manager_service - .sign_script_message(&wallet_spend_key.key_id, commitment.as_bytes()) + .sign_script_message(&pre_mine_script_key_id, commitment.as_bytes()) .await?; let out_dir = out_dir(&session_info.session_id, Context::Spend)?; let step_2_outputs_for_leader = PreMineSpendStep2OutputsForLeader { script_input_signature, - wallet_public_spend_key: wallet_spend_key.pub_key, public_script_nonce_key: script_nonce_key.pub_key, public_sender_offset_key: sender_offset_key.pub_key, public_sender_offset_nonce_key: sender_offset_nonce.pub_key, @@ -1403,6 +1420,14 @@ pub async fn command_runner( println!(); }, PreMineSpendEncumberAggregateUtxo(args) => { + match key_manager_service.get_wallet_type().await { + WalletType::Ledger(_) => {}, + _ => { + eprintln!("\nError: Wallet type must be 'Ledger' to spend pre-mine outputs!\n"); + break; + }, + } + // Read session info let session_info = read_verify_session_info::(&args.session_id)?; @@ -1489,6 +1514,14 @@ pub async fn command_runner( } }, PreMineSpendInputOutputSigs(args) => { + match key_manager_service.get_wallet_type().await { + WalletType::Ledger(_) => {}, + _ => { + eprintln!("\nError: Wallet type must be 'Ledger' to spend pre-mine outputs!\n"); + break; + }, + } + // Read session info let session_info = read_verify_session_info::(&args.session_id)?; // Read leader input @@ -1532,7 +1565,7 @@ pub async fn command_runner( // Metadata signature let script_offset = key_manager_service - .get_script_offset(&vec![party_info.wallet_spend_key_id], &vec![party_info + .get_script_offset(&vec![party_info.pre_mine_script_key_id], &vec![party_info .sender_offset_key_id .clone()]) .await?; @@ -1593,6 +1626,14 @@ pub async fn command_runner( } }, PreMineSpendAggregateTransaction(args) => { + match key_manager_service.get_wallet_type().await { + WalletType::Ledger(_) => {}, + _ => { + eprintln!("\nError: Wallet type must be 'Ledger' to spend pre-mine outputs!\n"); + break; + }, + } + // Read session info let session_info = read_verify_session_info::(&args.session_id)?; diff --git a/applications/minotari_console_wallet/src/automation/mod.rs b/applications/minotari_console_wallet/src/automation/mod.rs index 27ab569548..59caf40619 100644 --- a/applications/minotari_console_wallet/src/automation/mod.rs +++ b/applications/minotari_console_wallet/src/automation/mod.rs @@ -79,7 +79,6 @@ struct PreMineSpendStep2OutputsForSelf { #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] struct PreMineSpendStep2OutputsForLeader { script_input_signature: CheckSigSchnorrSignature, - wallet_public_spend_key: PublicKey, public_script_nonce_key: PublicKey, public_sender_offset_key: PublicKey, public_sender_offset_nonce_key: PublicKey, diff --git a/applications/minotari_ledger_wallet/comms/src/accessor_methods.rs b/applications/minotari_ledger_wallet/comms/src/accessor_methods.rs index 55244fec62..7e8dafc95c 100644 --- a/applications/minotari_ledger_wallet/comms/src/accessor_methods.rs +++ b/applications/minotari_ledger_wallet/comms/src/accessor_methods.rs @@ -48,7 +48,10 @@ pub fn verify_ledger_application() -> Result<(), LedgerDeviceError> { if let Ok(mut verified) = VERIFIED.try_lock() { if verified.is_none() { match verify() { - Ok(_) => *verified = Some(Ok(())), + Ok(_) => { + debug!(target: LOG_TARGET, "Ledger application 'Minotari Wallet' running and verified"); + *verified = Some(Ok(())) + }, Err(e) => return Err(e), } } 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 ac635c21c1..230101292a 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 @@ -21,10 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use derivative::Derivative; -use tari_common_types::{ - key_branches::TransactionKeyManagerBranch, - types::{ComAndPubSignature, PublicKey}, -}; +use tari_common_types::types::{ComAndPubSignature, PublicKey}; use tari_script::{ExecutionStack, TariScript}; use crate::{ @@ -264,9 +261,7 @@ impl WalletOutputBuilder { let aggregate_sender_offset_public_key = aggregated_sender_offset_public_key_shares + &sender_offset_public_key_self; - let ephemeral_pubkey_self = key_manager - .get_next_key(TransactionKeyManagerBranch::MetadataEphemeralNonce.get_branch_key()) - .await?; + let ephemeral_pubkey_self = key_manager.get_random_key().await?; let aggregate_ephemeral_pubkey = aggregated_ephemeral_public_key_shares + &ephemeral_pubkey_self.pub_key; let receiver_partial_metadata_signature = key_manager @@ -349,6 +344,7 @@ impl WalletOutputBuilder { #[cfg(test)] mod test { + use tari_common_types::key_branches::TransactionKeyManagerBranch; use tari_key_manager::key_manager_service::KeyManagerInterface; use super::*; diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 84f22f88e1..cb20287102 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -68,6 +68,7 @@ use tari_core::{ }; use tari_crypto::ristretto::pedersen::PedersenCommitment; use tari_key_manager::key_manager_service::SerializedKeyString; +use tari_key_manager::key_manager_service::{KeyAndId, KeyId}; use tari_script::{ inputs, push_pubkey_script, @@ -1185,6 +1186,32 @@ where Ok((tx_id, stp.into_transaction()?)) } + async fn pre_mine_script_key_from_payment_id( + &self, + payment_id: PaymentId, + tx_id: TxId, + ) -> Result, OutputManagerError> { + if let PaymentId::U64(index) = payment_id { + let script_key_id = KeyId::Managed { + branch: TransactionKeyManagerBranch::PreMine.get_branch_key(), + index, + }; + Ok(KeyAndId:: { + pub_key: self + .resources + .key_manager + .get_public_key_at_key_id(&script_key_id) + .await?, + key_id: script_key_id, + }) + } else { + Err(OutputManagerError::ServiceError(format!( + "Invalid payment id (TxId: {}): expected 'PaymentId::U64(_)', received {:?}", + tx_id, payment_id + ))) + } + } + /// Create a partial transaction in order to prepare output #[allow(clippy::too_many_lines)] #[allow(clippy::mutable_key_type)] @@ -1215,6 +1242,7 @@ where ), OutputManagerError, > { + trace!(target: LOG_TARGET, "encumber_aggregate_utxo: start"); // Fetch the output from the blockchain let output = self .fetch_unspent_outputs_from_node(vec![output_hash]) @@ -1232,63 +1260,62 @@ where tx_id ))); } + trace!(target: LOG_TARGET, "encumber_aggregate_utxo: fetched outputs"); // Retrieve the list of n public keys from the script - let public_keys = if let Some(Opcode::CheckMultiSigVerifyAggregatePubKey(_n, _m, keys, _msg)) = - output.script.as_slice().get(3) - { - keys.clone() - } else { - return Err(OutputManagerError::ServiceError(format!( - "Invalid script (TxId: {})", - tx_id - ))); - }; + let (multi_sig_public_keys, threshold) = get_multi_sig_script_components(&output.script, tx_id)?; + trace!(target: LOG_TARGET, "encumber_aggregate_utxo: retrieved public keys from script"); // Create a deterministic encryption key from the sum of the public keys - let sum_public_keys = public_keys + let sum_public_keys = multi_sig_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)?; let mut aggregated_script_public_key_shares = PublicKey::default(); + trace!(target: LOG_TARGET, "encumber_aggregate_utxo: created deterministic encryption key"); // Decrypt the output secrets and create a new input as WalletOutput (unblinded) - let input = if let Ok((amount, spending_key, payment_id)) = + let input = if let Ok((amount, commitment_mask, 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())? { + if output.verify_mask(&self.resources.factories.range_proof, &commitment_mask, amount.as_u64())? { + let script_key = self + .pre_mine_script_key_from_payment_id(payment_id.clone(), tx_id) + .await?; let mut script_signatures = Vec::new(); // lets add our own signature to the list let self_signature = self .resources .key_manager - .sign_script_message( - &self.resources.key_manager.get_spend_key().await?.key_id, - output.commitment.as_bytes(), - ) + .sign_script_message(&script_key.key_id, output.commitment.as_bytes()) .await?; - script_input_shares.insert( - self.resources.key_manager.get_spend_key().await?.pub_key, - self_signature, - ); + script_input_shares.insert(script_key.pub_key.clone(), self_signature); - // the order here is important, we need to add the signatures in the same order as public keys where + // the order here is important, we need to add the signatures in the same order as public keys were // added to the script originally - for key in public_keys { + for key in multi_sig_public_keys { if let Some(signature) = script_input_shares.get(&key) { script_signatures.push(StackItem::Signature(signature.clone())); - // our own key should not be added yet, it will be added with the script signing - if key != self.resources.key_manager.get_spend_key().await?.pub_key { + // our own key should not be aggregated yet, it will be added with the script signing + if key != script_key.pub_key { aggregated_script_public_key_shares = aggregated_script_public_key_shares + key; } } } - let spending_key_id = self.resources.key_manager.import_key(spending_key).await?; + if script_signatures.len() != usize::from(threshold) { + return Err(OutputManagerError::ServiceError(format!( + "Invalid number of signatures (TxId: {}), expected {}, received {}", + tx_id, + threshold, + script_signatures.len() + ))); + } + let commitment_mask_key_id = self.resources.key_manager.import_key(commitment_mask).await?; WalletOutput::new_with_rangeproof( output.version, amount, - spending_key_id, + commitment_mask_key_id, output.features, output.script, ExecutionStack::new(script_signatures), - self.resources.key_manager.get_spend_key().await?.key_id, // Only of the master wallet + script_key.key_id.clone(), // Only of the master wallet output.sender_offset_public_key, output.metadata_signature, 0, @@ -1310,6 +1337,8 @@ where tx_id ))); }; + trace!(target: LOG_TARGET, "encumber_aggregate_utxo: decrypt secrets, created unblinded input"); + trace!(target: LOG_TARGET, "encumber_aggregate_utxo: {:?}", input.input_data); // The entire input will be spent to a single recipient with no change let output_features = OutputFeatures { @@ -1330,6 +1359,7 @@ where let fee = self.get_fee_calc(); let fee = fee.calculate(fee_per_gram, 1, 1, 1, metadata_byte_size); let amount = input.value - fee; + trace!(target: LOG_TARGET, "encumber_aggregate_utxo: created script"); // Create sender transaction protocol builder with recipient data and no change let mut builder = SenderTransactionProtocol::builder( @@ -1362,6 +1392,14 @@ where .build() .await .map_err(|e| OutputManagerError::BuildError(e.message))?; + stp.change_recipient_sender_offset_private_key( + self.resources + .key_manager + .get_next_key(TransactionKeyManagerBranch::SenderOffsetLedger.get_branch_key()) + .await? + .key_id, + )?; + trace!(target: LOG_TARGET, "encumber_aggregate_utxo: created sender transaction protocol"); // This call is needed to advance the state from `SingleRoundMessageReady` to `SingleRoundMessageReady`, // but the returned value is not used @@ -1402,6 +1440,7 @@ where key_sum = key_sum + &PublicKey::from_vec(&shared_secret_self.as_bytes().to_vec())?; CommsDHKE::from_canonical_bytes(key_sum.as_bytes())? }; + trace!(target: LOG_TARGET, "encumber_aggregate_utxo: created dh shared secret"); let spending_key = shared_secret_to_output_spending_key(&shared_secret)?; let spending_key_id = self.resources.key_manager.import_key(spending_key).await?; @@ -1424,10 +1463,11 @@ where .await .map_err(|e| service_error_with_id(tx_id, e.to_string(), true))?, ); - let aggregated_metadata_ephemeral_public_key_shares = metadata_ephemeral_public_key_shares .iter() .fold(PublicKey::default(), |acc, x| acc + x); + trace!(target: LOG_TARGET, "encumber_aggregate_utxo: prepared inputs for partial metadata signature"); + // Create the output with a partially signed metadata signature let output = WalletOutputBuilder::new(amount, spending_key_id) .with_features( @@ -1462,6 +1502,7 @@ where .map_err(|e|service_error_with_id(tx_id, e.to_string(), true))?; let total_metadata_ephemeral_public_key = aggregated_metadata_ephemeral_public_key_shares + output.metadata_signature.ephemeral_pubkey(); + trace!(target: LOG_TARGET, "encumber_aggregate_utxo: created output with partial metadata signature"); // Finalize the partial transaction - it will not be valid at this stage as the metadata and script // signatures are not yet complete. @@ -1478,6 +1519,7 @@ where .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); + trace!(target: LOG_TARGET, "encumber_aggregate_utxo: finalized partial transaction"); let aggregated_script_signature_public_nonces = script_signature_public_nonces .iter() @@ -1491,6 +1533,7 @@ where &self.resources.key_manager, ) .await?; + trace!(target: LOG_TARGET, "encumber_aggregate_utxo: updated script input signature"); let total_script_nonce = aggregated_script_signature_public_nonces + updated_input.script_signature.ephemeral_pubkey(); @@ -1498,6 +1541,7 @@ where let mut tx_body = tx.body; tx_body.update_script_signature(updated_input.commitment()?, updated_input.script_signature.clone())?; tx.body = tx_body; + trace!(target: LOG_TARGET, "encumber_aggregate_utxo: updated script signature"); let fee = stp.get_fee_amount()?; @@ -3129,6 +3173,20 @@ where } } +fn get_multi_sig_script_components( + script: &TariScript, + tx_id: TxId, +) -> Result<(Vec, u8), OutputManagerError> { + if let Some(Opcode::CheckMultiSigVerifyAggregatePubKey(m, _n, keys, _msg)) = script.as_slice().get(3) { + Ok((keys.clone(), *m)) + } else { + Err(OutputManagerError::ServiceError(format!( + "Invalid script (TxId: {})", + tx_id + ))) + } +} + 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 { diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 3f6b1f3898..4fa9b0a5ca 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -102,15 +102,6 @@ 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: HashOutput, @@ -225,18 +216,6 @@ 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::SpendBackupPreMineUtxo { fee_per_gram, output_hash, @@ -728,34 +707,6 @@ 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), - } - } - #[allow(clippy::mutable_key_type)] pub async fn encumber_aggregate_utxo( &mut self, diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index a9bc397eb2..04364dd10a 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -39,7 +39,7 @@ use tari_common_types::{ key_branches::TransactionKeyManagerBranch, tari_address::{TariAddress, TariAddressFeatures}, transaction::{ImportStatus, TransactionDirection, TransactionStatus, TxId}, - types::{CommitmentFactory, FixedHash, HashOutput, PrivateKey, PublicKey, Signature}, + types::{CommitmentFactory, HashOutput, PrivateKey, PublicKey, Signature}, }; use tari_comms::{types::CommsPublicKey, NodeIdentity}; use tari_comms_dht::outbound::OutboundMessageRequester; @@ -48,20 +48,18 @@ use tari_core::{ covenants::Covenant, mempool::FeePerGramStat, one_sided::{ - public_key_to_output_encryption_key, shared_secret_to_output_encryption_key, shared_secret_to_output_spending_key, }, proto::{base_node as base_node_proto, base_node::FetchMatchingUtxos}, transactions::{ - key_manager::{TariKeyId, TransactionKeyManagerInterface}, + key_manager::TransactionKeyManagerInterface, tari_amount::MicroMinotari, transaction_components::{ encrypted_data::PaymentId, CodeTemplateRegistration, KernelFeatures, OutputFeatures, - RangeProofType, Transaction, TransactionOutput, WalletOutputBuilder, @@ -83,15 +81,7 @@ use tari_crypto::{ }; use tari_key_manager::key_manager_service::KeyId; use tari_p2p::domain_message::DomainMessage; -use tari_script::{ - push_pubkey_script, - script, - slice_to_boxed_message, - CheckSigSchnorrSignature, - ExecutionStack, - ScriptContext, - TariScript, -}; +use tari_script::{push_pubkey_script, script, CheckSigSchnorrSignature, ExecutionStack, ScriptContext, TariScript}; use tari_service_framework::{reply_channel, reply_channel::Receiver}; use tari_shutdown::ShutdownSignal; use tokio::{ @@ -701,29 +691,6 @@ 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, @@ -1188,218 +1155,6 @@ 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.interactive_tari_address.clone(), - self.resources.interactive_tari_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)) - } - async fn fetch_unspent_outputs_from_node( &mut self, hashes: Vec, @@ -1554,7 +1309,9 @@ where JoinHandle>>, >, ) -> Result { + trace!(target: LOG_TARGET, "finalized_aggregate_encumbed_tx: start"); let mut transaction = self.db.get_completed_transaction(tx_id)?; + trace!(target: LOG_TARGET, "finalized_aggregate_encumbed_tx: completed_transaction"); // Add the aggregate signature components transaction.transaction.script_offset = &transaction.transaction.script_offset + &script_offset; @@ -1563,11 +1320,13 @@ where &(transaction.transaction.body.outputs()[0].commitment.clone()), &transaction.transaction.body.outputs()[0].metadata_signature + &total_meta_data_signature, )?; + trace!(target: LOG_TARGET, "finalized_aggregate_encumbed_tx: updated metadata_signature"); transaction.transaction.body.update_script_signature( &(transaction.transaction.body.inputs()[0].commitment()?.clone()), &transaction.transaction.body.inputs()[0].script_signature + &total_script_data_signature, )?; + trace!(target: LOG_TARGET, "finalized_aggregate_encumbed_tx: updated script_signature"); // Validate the aggregate signatures and script offset let factory = CommitmentFactory::default(); @@ -1580,11 +1339,13 @@ where .commitment() .map_err(|e| TransactionServiceError::ServiceError(format!("TxId: {}, {}", tx_id, e)))?, ); + trace!(target: LOG_TARGET, "finalized_aggregate_encumbed_tx: input_data {:?}", input.input_data); input_keys = input_keys + input .run_and_verify_script(&factory, Some(context)) .map_err(|e| TransactionServiceError::ServiceError(format!("TxId: {}, {}", tx_id, e)))?; } + trace!(target: LOG_TARGET, "finalized_aggregate_encumbed_tx: validated inputs"); let mut output_keys = PublicKey::default(); for output in transaction.transaction.body.outputs() { output @@ -1592,6 +1353,7 @@ where .map_err(|e| TransactionServiceError::ServiceError(format!("TxId: {}, {}", tx_id, e)))?; output_keys = output_keys + output.sender_offset_public_key.clone(); } + trace!(target: LOG_TARGET, "finalized_aggregate_encumbed_tx: validated outputs"); let lhs = input_keys - output_keys; if lhs != PublicKey::from_secret_key(&transaction.transaction.script_offset) { return Err(TransactionServiceError::ServiceError(format!( @@ -1599,6 +1361,7 @@ where tx_id ))); } + trace!(target: LOG_TARGET, "finalized_aggregate_encumbed_tx: validated script offstet"); // Update the wallet database let _res = self From 8fdb9fdb7296c12d7c78a80294af5046824d1395 Mon Sep 17 00:00:00 2001 From: Hansie Odendaal Date: Wed, 31 Jul 2024 15:19:29 +0200 Subject: [PATCH 3/3] merge --- .../src/automation/commands.rs | 36 ++++++---- .../minotari_console_wallet/src/cli.rs | 4 +- .../minotari_console_wallet/src/init/mod.rs | 8 +-- .../src/wallet_modes.rs | 3 +- .../comms/examples/ledger_demo/main.rs | 2 +- base_layer/common_types/src/key_branches.rs | 12 ++-- .../src/transactions/key_manager/inner.rs | 65 ++++++++++--------- .../core/src/transactions/key_manager/mod.rs | 1 + .../src/output_manager_service/service.rs | 5 +- .../wallet/src/transaction_service/service.rs | 5 +- 10 files changed, 72 insertions(+), 69 deletions(-) diff --git a/applications/minotari_console_wallet/src/automation/commands.rs b/applications/minotari_console_wallet/src/automation/commands.rs index 94cdb0ea0a..3e96f3abe8 100644 --- a/applications/minotari_console_wallet/src/automation/commands.rs +++ b/applications/minotari_console_wallet/src/automation/commands.rs @@ -1268,22 +1268,28 @@ pub async fn command_runner( println!(); }, PreMineSpendBackupUtxo(args) => { - let commitment = if let Ok(val) = Commitment::from_hex(&args.commitment) { - val - } else { - eprintln!("\nError: Invalid 'commitment' provided!\n"); - continue; - }; - let hash = if let Ok(val) = FixedHash::from_hex(&args.output_hash) { - val - } else { - eprintln!("\nError: Invalid 'output_hash' provided!\n"); - continue; + match key_manager_service.get_wallet_type().await { + WalletType::Ledger(_) => {}, + _ => { + eprintln!("\nError: Wallet type must be 'Ledger' to spend pre-mine outputs!\n"); + break; + }, + } + + let embedded_output = match get_embedded_pre_mine_outputs(vec![args.output_index]) { + Ok(outputs) => outputs[0].clone(), + Err(e) => { + eprintln!("\nError: {}\n", e); + break; + }, }; + let commitment = embedded_output.commitment.clone(); + let output_hash = embedded_output.hash(); + match spend_backup_pre_mine_utxo( transaction_service.clone(), args.fee_per_gram, - hash, + output_hash, commitment.clone(), args.recipient_address, ) @@ -1294,10 +1300,12 @@ pub async fn command_runner( println!("Spend utxo: {} with tx_id: {}", commitment.to_hex(), tx_id); println!(); }, - Err(e) => eprintln!("\nError:Spent pre-mine transaction error! {}\n", e), + Err(e) => { + eprintln!("\nError: Spent pre-mine transaction error! {}\n", e); + break; + }, } }, - PreMineCreatePartyDetails(args) => { PreMineSpendPartyDetails(args) => { match key_manager_service.get_wallet_type().await { WalletType::Ledger(_) => {}, diff --git a/applications/minotari_console_wallet/src/cli.rs b/applications/minotari_console_wallet/src/cli.rs index f38d3cc337..59dc54c21d 100644 --- a/applications/minotari_console_wallet/src/cli.rs +++ b/applications/minotari_console_wallet/src/cli.rs @@ -242,9 +242,7 @@ pub struct PreMineSpendBackupUtxoArgs { #[clap(long)] pub fee_per_gram: MicroMinotari, #[clap(long)] - pub commitment: String, - #[clap(long)] - pub output_hash: String, + pub output_index: usize, #[clap(long)] pub recipient_address: TariAddress, } diff --git a/applications/minotari_console_wallet/src/init/mod.rs b/applications/minotari_console_wallet/src/init/mod.rs index b548e41d35..2fe925622e 100644 --- a/applications/minotari_console_wallet/src/init/mod.rs +++ b/applications/minotari_console_wallet/src/init/mod.rs @@ -64,7 +64,7 @@ use tari_comms::{ use tari_core::{ consensus::ConsensusManager, transactions::{ - key_manager::{TariKeyId, TransactionKeyManagerInterface}, + key_manager::{TariKeyId, TransactionKeyManagerInterface, LEDGER_NOT_SUPPORTED}, transaction_components::TransactionError, CryptoFactories, }, @@ -576,9 +576,7 @@ pub async fn start_wallet( { return Err(ExitError::new( ExitCode::WalletError, - "Ledger is not supported in this build, please enable the \"ledger\" feature for console wallet and \ - core" - .to_string(), + format!("{}", LEDGER_NOT_SUPPORTED), )); } @@ -591,7 +589,7 @@ pub async fn start_wallet( match wallet.key_manager_service.get_public_key_at_key_id(&key_id).await { Ok(public_key) => {}, Err(e) => { - if e.to_string().contains("Ledger is not supported in this build") { + if e.to_string().contains(LEDGER_NOT_SUPPORTED) { return Err(ExitError::new(ExitCode::WalletError, format!(" {}", e))); } }, diff --git a/applications/minotari_console_wallet/src/wallet_modes.rs b/applications/minotari_console_wallet/src/wallet_modes.rs index c75440dd16..565eaa5c24 100644 --- a/applications/minotari_console_wallet/src/wallet_modes.rs +++ b/applications/minotari_console_wallet/src/wallet_modes.rs @@ -177,7 +177,8 @@ fn force_exit_for_pre_mine_commands(command: &CliCommands) -> bool { CliCommands::PreMineSpendEncumberAggregateUtxo(_) | CliCommands::PreMineSpendAggregateTransaction(_) | CliCommands::PreMineSpendPartyDetails(_) | - CliCommands::PreMineSpendInputOutputSigs(_) + CliCommands::PreMineSpendInputOutputSigs(_) | + CliCommands::PreMineSpendBackupUtxo(_) ) } diff --git a/applications/minotari_ledger_wallet/comms/examples/ledger_demo/main.rs b/applications/minotari_ledger_wallet/comms/examples/ledger_demo/main.rs index 6197f52c40..b1b4db696e 100644 --- a/applications/minotari_ledger_wallet/comms/examples/ledger_demo/main.rs +++ b/applications/minotari_ledger_wallet/comms/examples/ledger_demo/main.rs @@ -116,7 +116,7 @@ fn main() { let index = OsRng.next_u64(); for branch in &[ - TransactionKeyManagerBranch::SenderOffsetLedger, + TransactionKeyManagerBranch::OneSidedSenderOffset, TransactionKeyManagerBranch::Spend, TransactionKeyManagerBranch::RandomKey, TransactionKeyManagerBranch::PreMine, diff --git a/base_layer/common_types/src/key_branches.rs b/base_layer/common_types/src/key_branches.rs index c90b684731..1df25b593c 100644 --- a/base_layer/common_types/src/key_branches.rs +++ b/base_layer/common_types/src/key_branches.rs @@ -98,7 +98,7 @@ impl TransactionKeyManagerBranch { Some(Branch::Nonce) => Some(TransactionKeyManagerBranch::Nonce), Some(Branch::KernelNonce) => Some(TransactionKeyManagerBranch::KernelNonce), Some(Branch::SenderOffset) => Some(TransactionKeyManagerBranch::SenderOffset), - Some(Branch::SenderOffsetLedger) => Some(TransactionKeyManagerBranch::SenderOffsetLedger), + Some(Branch::OneSidedSenderOffset) => Some(TransactionKeyManagerBranch::OneSidedSenderOffset), Some(Branch::Spend) => Some(TransactionKeyManagerBranch::Spend), Some(Branch::RandomKey) => Some(TransactionKeyManagerBranch::RandomKey), Some(Branch::PreMine) => Some(TransactionKeyManagerBranch::PreMine), @@ -119,10 +119,10 @@ mod test { KERNEL_NONCE, METADATA_EPHEMERAL_NONCE, NONCE, + ONE_SIDED_SENDER_OFFSET, PRE_MINE, RANDOM_KEY, SENDER_OFFSET, - SENDER_OFFSET_LEDGER, }, WALLET_COMMS_AND_SPEND_KEY_BRANCH, }; @@ -158,9 +158,9 @@ mod test { SENDER_OFFSET, ), ( - Branch::SenderOffsetLedger as u8, - TransactionKeyManagerBranch::SenderOffsetLedger, - SENDER_OFFSET_LEDGER, + Branch::OneSidedSenderOffset as u8, + TransactionKeyManagerBranch::OneSidedSenderOffset, + ONE_SIDED_SENDER_OFFSET, ), ( Branch::Spend as u8, @@ -213,7 +213,7 @@ mod test { assert_eq!(&branch.get_branch_key(), *key); assert_eq!(TransactionKeyManagerBranch::from_key(key), *branch); }, - TransactionKeyManagerBranch::SenderOffsetLedger => { + TransactionKeyManagerBranch::OneSidedSenderOffset => { assert_eq!(branch.as_byte(), *expected_byte); assert_eq!(TransactionKeyManagerBranch::from_byte(*expected_byte), Some(*branch)); assert_eq!(&branch.get_branch_key(), *key); diff --git a/base_layer/core/src/transactions/key_manager/inner.rs b/base_layer/core/src/transactions/key_manager/inner.rs index 08eb5c5049..93515bec3a 100644 --- a/base_layer/core/src/transactions/key_manager/inner.rs +++ b/base_layer/core/src/transactions/key_manager/inner.rs @@ -76,6 +76,8 @@ const LOG_TARGET: &str = "c::bn::key_manager::key_manager_service"; const TRANSACTION_KEY_MANAGER_MAX_SEARCH_DEPTH: u64 = 1_000_000; const HASHER_LABEL_STEALTH_KEY: &str = "script key"; +pub const LEDGER_NOT_SUPPORTED: &str = "Ledger is not supported in this build, please enable the \"ledger\" feature."; + use crate::{ common::ConfidentialOutputHasher, one_sided::diffie_hellman_stealth_domain_hasher, @@ -188,12 +190,12 @@ where TBackend: KeyManagerBackend + 'static pub async fn get_random_key(&self) -> Result, KeyManagerServiceError> { match &self.wallet_type { WalletType::Ledger(ledger) => { - debug!(target: LOG_TARGET, "get_random_key: wallet type {}", self.wallet_type); + debug!(target: LOG_TARGET, "get_random_key: {}", self.wallet_type); #[cfg(not(feature = "ledger"))] { Err(KeyManagerServiceError::LedgerError(format!( - "Ledger {} is not supported in this build, please enable the \"ledger\" feature for core", - ledger + "{} 'get_random_key' was called. ({})", + LEDGER_NOT_SUPPORTED, ledger ))) } #[cfg(feature = "ledger")] @@ -240,13 +242,13 @@ where TBackend: KeyManagerBackend + 'static if let WalletType::Ledger(ledger) = &self.wallet_type { match TransactionKeyManagerBranch::from_key(branch) { TransactionKeyManagerBranch::OneSidedSenderOffset | TransactionKeyManagerBranch::RandomKey => { + debug!(target: LOG_TARGET, "get_public_key_at_key_id: {}", self.wallet_type); #[cfg(not(feature = "ledger"))] { - Err(KeyManagerServiceError::LedgerError( - "Ledger is not supported in this build, please enable the \"ledger\" feature for \ - core" - .to_string(), - )) + Err(KeyManagerServiceError::LedgerError(format!( + "{} 'get_public_key_at_key_id' was called.", + LEDGER_NOT_SUPPORTED + ))) } #[cfg(feature = "ledger")] @@ -667,13 +669,12 @@ where TBackend: KeyManagerBackend + 'static if let KeyId::Managed { branch, index } = secret_key_id { match TransactionKeyManagerBranch::from_key(branch) { TransactionKeyManagerBranch::OneSidedSenderOffset | TransactionKeyManagerBranch::RandomKey => { - debug!(target: LOG_TARGET, "get_diffie_hellman_shared_secret: wallet type {}", self.wallet_type); + debug!(target: LOG_TARGET, "get_diffie_hellman_shared_secret: {}", self.wallet_type); #[cfg(not(feature = "ledger"))] { return Err(TransactionError::LedgerNotSupported(format!( - "Ledger {} (has index {}) is not supported in this build, please enable the \ - \"ledger\" feature for core", - ledger, index + "{} 'get_diffie_hellman_shared_secret' was called. ({} (has index {}))", + LEDGER_NOT_SUPPORTED, ledger, index ))); } @@ -707,13 +708,12 @@ where TBackend: KeyManagerBackend + 'static WalletType::Ledger(ledger) => match secret_key_id { KeyId::Managed { branch, index } => match TransactionKeyManagerBranch::from_key(branch) { TransactionKeyManagerBranch::OneSidedSenderOffset => { - debug!(target: LOG_TARGET, "get_diffie_hellman_stealth_domain_hasher: allet type {}", self.wallet_type); + debug!(target: LOG_TARGET, "get_diffie_hellman_stealth_domain_hasher: {}", self.wallet_type); #[cfg(not(feature = "ledger"))] { Err(TransactionError::LedgerNotSupported(format!( - "Ledger {} (has index {}) is not supported in this build, please enable the \ - \"ledger\" feature for core", - ledger, index + "{} 'get_diffie_hellman_stealth_domain_hasher' was called. ({} (has index {}))", + LEDGER_NOT_SUPPORTED, ledger, index ))) } @@ -792,13 +792,12 @@ where TBackend: KeyManagerBackend + 'static match (&self.wallet_type, script_key_id) { (WalletType::Ledger(ledger), KeyId::Derived { key }) => { - debug!(target: LOG_TARGET, "get_script_signature: wallet type {}", self.wallet_type); + debug!(target: LOG_TARGET, "get_script_signature: {}", self.wallet_type); #[cfg(not(feature = "ledger"))] { Err(TransactionError::LedgerNotSupported(format!( - "Ledger {} (has script_key_id {}, branch {}, index {}) is not supported in this build, please \ - enable the \"ledger\" feature for core", - ledger, script_key_id, branch, index + "{} 'get_script_signature' was called. ({} (has key {}))", + LEDGER_NOT_SUPPORTED, ledger, key ))) } @@ -993,12 +992,12 @@ where TBackend: KeyManagerBackend + 'static Ok(script_offset) }, WalletType::Ledger(ledger) => { - debug!(target: LOG_TARGET, "get_script_offset: wallet type {}", self.wallet_type); + debug!(target: LOG_TARGET, "get_script_offset: {}", self.wallet_type); #[cfg(not(feature = "ledger"))] { Err(TransactionError::LedgerNotSupported(format!( - "Ledger {} is not supported in this build, please enable the \"ledger\" feature for core", - ledger + "{} 'get_script_offset' was called. ({})", + LEDGER_NOT_SUPPORTED, ledger ))) } @@ -1082,12 +1081,12 @@ where TBackend: KeyManagerBackend + 'static ) -> Result { match &self.wallet_type { WalletType::Ledger(ledger) => { - debug!(target: LOG_TARGET, "sign_script_message: wallet type {}", self.wallet_type); + debug!(target: LOG_TARGET, "sign_script_message: {}", self.wallet_type); #[cfg(not(feature = "ledger"))] { Err(TransactionError::LedgerNotSupported(format!( - "Ledger {} is not supported in this build, please enable the \"ledger\" feature for core", - ledger + "{} 'sign_script_message' was called. ({})", + LEDGER_NOT_SUPPORTED, ledger ))) } @@ -1127,12 +1126,12 @@ where TBackend: KeyManagerBackend + 'static ) -> Result { match &self.wallet_type { WalletType::Ledger(ledger) => { - debug!(target: LOG_TARGET, "sign_with_nonce_and_challenge: wallet type {}", self.wallet_type); + debug!(target: LOG_TARGET, "sign_with_nonce_and_challenge: {}", self.wallet_type); #[cfg(not(feature = "ledger"))] { Err(TransactionError::LedgerNotSupported(format!( - "Ledger {} is not supported in this build, please enable the \"ledger\" feature for core", - ledger + "{} 'sign_with_nonce_and_challenge' was called. ({})", + LEDGER_NOT_SUPPORTED, ledger ))) } @@ -1245,11 +1244,13 @@ where TBackend: KeyManagerBackend + 'static .await }, WalletType::Ledger(ledger) => { + debug!(target: LOG_TARGET, "get_one_sided_metadata_signature: {}", self.wallet_type); #[cfg(not(feature = "ledger"))] { - Err(TransactionError::LedgerNotSupported( - "One sided metadata signature was called for ledger, but ledger is not supported.".to_string(), - )) + Err(TransactionError::LedgerNotSupported(format!( + "{} 'get_one_sided_metadata_signature' was called. ({})", + LEDGER_NOT_SUPPORTED, ledger + ))) } #[cfg(feature = "ledger")] diff --git a/base_layer/core/src/transactions/key_manager/mod.rs b/base_layer/core/src/transactions/key_manager/mod.rs index b1c356c574..37fe079630 100644 --- a/base_layer/core/src/transactions/key_manager/mod.rs +++ b/base_layer/core/src/transactions/key_manager/mod.rs @@ -36,6 +36,7 @@ mod initializer; pub use initializer::TransactionKeyManagerInitializer; mod inner; +pub use inner::LEDGER_NOT_SUPPORTED; /// This is a memory database implementation of the `TransactionKeyManager` trait. mod memory_db_key_manager; pub use inner::TransactionKeyManagerInner; diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index cb20287102..8495bc7982 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -67,8 +67,7 @@ use tari_core::{ }, }; use tari_crypto::ristretto::pedersen::PedersenCommitment; -use tari_key_manager::key_manager_service::SerializedKeyString; -use tari_key_manager::key_manager_service::{KeyAndId, KeyId}; +use tari_key_manager::key_manager_service::{KeyAndId, KeyId, SerializedKeyString}; use tari_script::{ inputs, push_pubkey_script, @@ -1395,7 +1394,7 @@ where stp.change_recipient_sender_offset_private_key( self.resources .key_manager - .get_next_key(TransactionKeyManagerBranch::SenderOffsetLedger.get_branch_key()) + .get_next_key(TransactionKeyManagerBranch::OneSidedSenderOffset.get_branch_key()) .await? .key_id, )?; diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 04364dd10a..3203d91c97 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -47,10 +47,7 @@ use tari_core::{ consensus::ConsensusManager, covenants::Covenant, mempool::FeePerGramStat, - one_sided::{ - shared_secret_to_output_encryption_key, - shared_secret_to_output_spending_key, - }, + one_sided::{shared_secret_to_output_encryption_key, shared_secret_to_output_spending_key}, proto::{base_node as base_node_proto, base_node::FetchMatchingUtxos}, transactions::{ key_manager::TransactionKeyManagerInterface,