From 5a5c8c84b9b488a6a613aba5ae9a898d755341ed Mon Sep 17 00:00:00 2001 From: Hansie Odendaal Date: Thu, 25 Jul 2024 05:06:00 +0200 Subject: [PATCH] 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 +- .../comms/src/ledger_wallet.rs | 2 +- .../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 | 37 +- .../src/transactions/key_manager/wrapper.rs | 5 + 22 files changed, 1783 insertions(+), 352 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 1795b0d4e1..b72c455737 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}, }; @@ -49,12 +51,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}, @@ -63,6 +68,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, @@ -70,6 +76,7 @@ use tari_core::{ transaction_components::{ encrypted_data::PaymentId, OutputFeatures, + RangeProofType, Transaction, TransactionInput, TransactionInputVersion, @@ -78,14 +85,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}, @@ -97,20 +105,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}, @@ -119,12 +131,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 {} @@ -755,56 +769,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) => { @@ -838,13 +1283,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?; @@ -853,9 +1299,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, @@ -867,31 +1340,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)?; @@ -901,13 +1394,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(); @@ -917,9 +1410,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); @@ -948,13 +1444,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, @@ -973,31 +1469,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, )?; @@ -1012,20 +1511,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 @@ -1048,8 +1547,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, @@ -1057,25 +1555,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)?; @@ -1083,31 +1583,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, )?; @@ -1541,6 +2044,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 51d28584ad..4432e4ea13 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), @@ -164,13 +167,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)] @@ -178,15 +205,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)] @@ -194,13 +223,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 a3e3b6d688..2f93972a68 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, @@ -620,11 +639,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 00b7c0780e..142263c00e 100644 --- a/applications/minotari_ledger_wallet/common/src/common_types.rs +++ b/applications/minotari_ledger_wallet/common/src/common_types.rs @@ -119,6 +119,7 @@ pub enum Branch { SenderOffsetLedger = 0x06, Spend = 0x07, RandomKey = 0x08, + PreMine = 0x09, } impl Branch { @@ -137,6 +138,7 @@ impl Branch { 0x06 => Some(Branch::SenderOffsetLedger), 0x07 => Some(Branch::Spend), 0x08 => Some(Branch::RandomKey), + 0x09 => Some(Branch::PreMine), _ => None, } } @@ -288,6 +290,7 @@ mod test { (0x06, Branch::SenderOffsetLedger), (0x07, Branch::Spend), (0x08, Branch::RandomKey), + (0x09, Branch::PreMine), ]; for (expected_byte, branch) in &mappings { @@ -328,6 +331,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 a66acef66d..0f9594b293 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 75d48b1c6b..abf94f2d33 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(); @@ -288,6 +291,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/comms/src/ledger_wallet.rs b/applications/minotari_ledger_wallet/comms/src/ledger_wallet.rs index b192045b1d..3ec841f3e0 100644 --- a/applications/minotari_ledger_wallet/comms/src/ledger_wallet.rs +++ b/applications/minotari_ledger_wallet/comms/src/ledger_wallet.rs @@ -31,7 +31,7 @@ use tari_utilities::ByteArray; use crate::error::LedgerDeviceError; pub const EXPECTED_NAME: &str = "minotari_ledger_wallet"; -pub const EXPECTED_VERSION: &str = "1.0.0-pre.16"; +pub const EXPECTED_VERSION: &str = "1.0.0-pre.18"; const WALLET_CLA: u8 = 0x80; pub fn get_transport() -> Result { diff --git a/applications/minotari_ledger_wallet/wallet/src/main.rs b/applications/minotari_ledger_wallet/wallet/src/main.rs index 4a6b861b92..658028d29f 100644 --- a/applications/minotari_ledger_wallet/wallet/src/main.rs +++ b/applications/minotari_ledger_wallet/wallet/src/main.rs @@ -143,6 +143,7 @@ pub enum KeyType { ViewKey = 0x03, OneSidedSenderOffset = 0x04, Random = 0x06, + PreMine = 0x07, } impl KeyType { @@ -159,6 +160,7 @@ impl KeyType { BranchMapping::SenderOffsetLedger => 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 7af11f3d2e..9f463021a5 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 { SenderOffsetLedger = Branch::SenderOffsetLedger 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 SENDER_OFFSET_LEDGER: &str = "sender offset ledger"; 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::SenderOffsetLedger => SENDER_OFFSET_LEDGER.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 { SENDER_OFFSET_LEDGER => TransactionKeyManagerBranch::SenderOffsetLedger, 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 bd0a520c73..1aa0f88cc8 100644 --- a/base_layer/core/src/transactions/key_manager/inner.rs +++ b/base_layer/core/src/transactions/key_manager/inner.rs @@ -189,10 +189,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 ))) } @@ -241,11 +242,16 @@ where TBackend: KeyManagerBackend + 'static // If we have the unique case of being a ledger wallet, and the key is a Managed EphemeralNonce, or // SenderOffset than we fetch from the ledger, all other keys are fetched below. WalletType::Ledger(ledger) => match TransactionKeyManagerBranch::from_key(branch) { - TransactionKeyManagerBranch::SenderOffsetLedger | TransactionKeyManagerBranch::RandomKey => { + TransactionKeyManagerBranch::SenderOffsetLedger | + TransactionKeyManagerBranch::RandomKey | + TransactionKeyManagerBranch::PreMine => { + debug!(target: LOG_TARGET, "get_public_key_at_key_id: wallet type {}", self.wallet_type); #[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(), )) } @@ -413,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(), @@ -685,10 +695,12 @@ where TBackend: KeyManagerBackend + 'static WalletType::Ledger(ledger) => match secret_key_id { KeyId::Managed { branch, index } => match TransactionKeyManagerBranch::from_key(branch) { TransactionKeyManagerBranch::SenderOffsetLedger => { + debug!(target: LOG_TARGET, "get_diffie_hellman_shared_secret: wallet 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 ))) } @@ -728,10 +740,12 @@ where TBackend: KeyManagerBackend + 'static WalletType::Ledger(ledger) => match secret_key_id { KeyId::Managed { branch, index } => match TransactionKeyManagerBranch::from_key(branch) { TransactionKeyManagerBranch::SenderOffsetLedger => { + 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 ))) } @@ -827,10 +841,12 @@ where TBackend: KeyManagerBackend + 'static index, }, ) => { + 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 {}, branch {}, index {}) is not supported", + "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 ))) } @@ -1041,10 +1057,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 ))) } @@ -1124,10 +1141,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 ))) } @@ -1168,10 +1186,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 0701391f2d..f2644962c3 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]