diff --git a/Cargo.lock b/Cargo.lock index 7a37e412a6..3f3c29d6f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3169,6 +3169,7 @@ dependencies = [ "serde", "serde_json", "tari_common", + "tari_common_types", "tari_comms", "tari_core", "tari_features", @@ -5826,6 +5827,7 @@ dependencies = [ "tari_contacts", "tari_core", "tari_crypto", + "tari_key_manager", "tari_p2p", "tari_script", "tari_shutdown", diff --git a/applications/minotari_app_grpc/proto/wallet.proto b/applications/minotari_app_grpc/proto/wallet.proto index d6919f55d7..82a4ff13f1 100644 --- a/applications/minotari_app_grpc/proto/wallet.proto +++ b/applications/minotari_app_grpc/proto/wallet.proto @@ -249,6 +249,8 @@ message GetCoinbaseRequest { uint64 fee = 2; uint64 height = 3; bytes extra = 4; + bytes wallet_payment_address = 5; + bool stealth_payment = 6; } message GetCoinbaseResponse { diff --git a/applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs b/applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs index 8997f0294b..654ba8b853 100644 --- a/applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -315,9 +315,18 @@ impl wallet_server::Wallet for WalletGrpcServer { ) -> Result, Status> { let request = request.into_inner(); let mut tx_service = self.get_transaction_service(); + let wallet_payment_address = + TariAddress::from_bytes(&request.wallet_payment_address).map_err(|e| Status::invalid_argument(e.to_string()))?; let coinbase = tx_service - .generate_coinbase_transaction(request.reward.into(), request.fee.into(), request.height, request.extra) + .generate_coinbase_transaction( + wallet_payment_address, + request.stealth_payment, + request.reward.into(), + request.fee.into(), + request.height, + request.extra, + ) .await .map_err(|err| Status::unknown(err.to_string()))?; diff --git a/applications/minotari_merge_mining_proxy/Cargo.toml b/applications/minotari_merge_mining_proxy/Cargo.toml index 67097ef679..e6a381bf8d 100644 --- a/applications/minotari_merge_mining_proxy/Cargo.toml +++ b/applications/minotari_merge_mining_proxy/Cargo.toml @@ -12,6 +12,7 @@ default = [] [dependencies] tari_common = { path = "../../common" } +tari_common_types = { path = "../../base_layer/common_types" } tari_comms = { path = "../../comms/core" } tari_core = { path = "../../base_layer/core", default-features = false, features = ["transactions"] } minotari_app_utilities = { path = "../minotari_app_utilities" } diff --git a/applications/minotari_merge_mining_proxy/src/block_template_protocol.rs b/applications/minotari_merge_mining_proxy/src/block_template_protocol.rs index 38fbe6640c..41f31bcabc 100644 --- a/applications/minotari_merge_mining_proxy/src/block_template_protocol.rs +++ b/applications/minotari_merge_mining_proxy/src/block_template_protocol.rs @@ -25,8 +25,12 @@ use std::{cmp, sync::Arc}; use log::*; +use tari_utilities::ByteArray; +use tari_utilities::hex::Hex; use minotari_node_grpc_client::{grpc, BaseNodeGrpcClient}; use minotari_wallet_grpc_client::WalletGrpcClient; +use tari_common_types::tari_address::TariAddress; +use tari_common_types::types::PublicKey; use tari_core::proof_of_work::{monero_rx, monero_rx::FixedByteArray, Difficulty}; use crate::{ @@ -185,6 +189,9 @@ impl BlockTemplateProtocol<'_> { let block_reward = miner_data.reward; let total_fees = miner_data.total_fees; let extra = self.config.coinbase_extra.as_bytes().to_vec(); + let wallet_payment_address = TariAddress::from_hex(&self.config.wallet_payment_address) + .map_err(|e| MmProxyError::ConversionError(e.to_string()))? + .to_vec(); let coinbase_response = self .wallet_client @@ -193,6 +200,8 @@ impl BlockTemplateProtocol<'_> { fee: total_fees, height: tari_height, extra, + wallet_payment_address, + stealth_payment: self.config.stealth_payment, }) .await .map_err(|status| MmProxyError::GrpcRequestError { diff --git a/applications/minotari_merge_mining_proxy/src/config.rs b/applications/minotari_merge_mining_proxy/src/config.rs index 2eddef5de1..b81be071c6 100644 --- a/applications/minotari_merge_mining_proxy/src/config.rs +++ b/applications/minotari_merge_mining_proxy/src/config.rs @@ -26,6 +26,7 @@ use tari_common::{ configuration::{Network, StringList}, SubConfigPath, }; +use tari_common_types::tari_address::TariAddress; use tari_comms::multiaddr::Multiaddr; #[derive(Clone, Debug, Deserialize, Serialize)] @@ -69,6 +70,11 @@ pub struct MergeMiningProxyConfig { pub coinbase_extra: String, /// Selected network pub network: Network, + /// The Tari wallet address where the mining funds will be sent to (must be different wallet from the one used for + /// the wallet_grpc_address) + pub wallet_payment_address: String, + /// Stealth payment yes or no + pub stealth_payment: bool, } impl Default for MergeMiningProxyConfig { @@ -89,6 +95,8 @@ impl Default for MergeMiningProxyConfig { max_randomx_vms: 5, coinbase_extra: "tari_merge_mining_proxy".to_string(), network: Default::default(), + wallet_payment_address: TariAddress::default().to_hex(), + stealth_payment: true, } } } diff --git a/applications/minotari_miner/src/config.rs b/applications/minotari_miner/src/config.rs index ca9101ddde..b4447d8528 100644 --- a/applications/minotari_miner/src/config.rs +++ b/applications/minotari_miner/src/config.rs @@ -41,7 +41,7 @@ use std::time::Duration; use minotari_app_grpc::tari_rpc::{pow_algo::PowAlgos, NewBlockTemplateRequest, PowAlgo}; use serde::{Deserialize, Serialize}; use tari_common::{configuration::Network, SubConfigPath}; -use tari_common_types::grpc_authentication::GrpcAuthentication; +use tari_common_types::{grpc_authentication::GrpcAuthentication, tari_address::TariAddress}; use tari_comms::multiaddr::Multiaddr; #[derive(Serialize, Deserialize, Debug)] @@ -77,6 +77,11 @@ pub struct MinerConfig { pub network: Network, /// Base node reconnect timeout after any GRPC or miner error pub wait_timeout_on_error: u64, + /// The Tari wallet address where the mining funds will be sent to (must be different wallet from the one used for + /// the wallet_grpc_address) + pub wallet_payment_address: String, + /// Stealth payment yes or no + pub stealth_payment: bool, } /// The proof of work data structure that is included in the block header. For the Minotari miner only `Sha3x` is @@ -109,6 +114,8 @@ impl Default for MinerConfig { coinbase_extra: "minotari_miner".to_string(), network: Default::default(), wait_timeout_on_error: 10, + wallet_payment_address: TariAddress::default().to_hex(), + stealth_payment: true, } } } diff --git a/applications/minotari_miner/src/run_miner.rs b/applications/minotari_miner/src/run_miner.rs index 815ccfe757..b856cdbfec 100644 --- a/applications/minotari_miner/src/run_miner.rs +++ b/applications/minotari_miner/src/run_miner.rs @@ -43,6 +43,8 @@ use tonic::{ codegen::InterceptedService, transport::{Channel, Endpoint}, }; +use tari_common_types::tari_address::TariAddress; +use tari_common_types::types::PublicKey; use crate::{ cli::Cli, @@ -245,7 +247,13 @@ async fn mining_cycle( } debug!(target: LOG_TARGET, "Getting coinbase"); - let request = coinbase_request(&template, config.coinbase_extra.as_bytes().to_vec())?; + let request = coinbase_request( + &template, + config.coinbase_extra.as_bytes().to_vec(), + &TariAddress::from_hex(&config.wallet_payment_address) + .map_err(|e| MinerError::Conversion(e.to_string()))?, + config.stealth_payment, + )?; let coinbase = wallet_conn.get_coinbase(request).await?.into_inner(); let (output, kernel) = extract_outputs_and_kernels(coinbase)?; let body = block_template diff --git a/applications/minotari_miner/src/utils.rs b/applications/minotari_miner/src/utils.rs index a7f3794a4a..e609592e1d 100644 --- a/applications/minotari_miner/src/utils.rs +++ b/applications/minotari_miner/src/utils.rs @@ -12,14 +12,14 @@ // 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote // products derived from this software without specific prior written permission. // -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +// DAMAGE. use minotari_app_grpc::tari_rpc::{ GetCoinbaseRequest, GetCoinbaseResponse, @@ -27,6 +27,9 @@ use minotari_app_grpc::tari_rpc::{ TransactionKernel, TransactionOutput, }; +use tari_common_types::types::PublicKey; +use tari_utilities::ByteArray; +use tari_common_types::tari_address::TariAddress; use crate::errors::{err_empty, MinerError}; @@ -34,6 +37,8 @@ use crate::errors::{err_empty, MinerError}; pub fn coinbase_request( template_response: &NewBlockTemplateResponse, extra: Vec, + wallet_payment_address: &TariAddress, + stealth_payment: bool, ) -> Result { let template = template_response .new_block_template @@ -50,11 +55,14 @@ pub fn coinbase_request( .as_ref() .ok_or_else(|| err_empty("template.header"))? .height; + let wallet_payment_address = wallet_payment_address.as_bytes().to_vec(); Ok(GetCoinbaseRequest { reward, fee, height, extra, + wallet_payment_address, + stealth_payment, }) } diff --git a/base_layer/core/src/transactions/coinbase_builder.rs b/base_layer/core/src/transactions/coinbase_builder.rs index d844cbe442..df158945c8 100644 --- a/base_layer/core/src/transactions/coinbase_builder.rs +++ b/base_layer/core/src/transactions/coinbase_builder.rs @@ -21,7 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // -use tari_common_types::types::{Commitment, PrivateKey}; +use tari_common_types::types::{Commitment, PrivateKey, PublicKey}; use tari_key_manager::key_manager_service::KeyManagerServiceError; use tari_script::{inputs, script, TariScript}; use thiserror::Error; @@ -64,6 +64,14 @@ pub enum CoinbaseBuildError { MissingSpendKey, #[error("The script key for this coinbase transaction wasn't provided")] MissingScriptKey, + #[error("The script for this coinbase transaction wasn't provided")] + MissingScript, + #[error("The wallet public key for this coinbase transaction wasn't provided")] + MissingWalletPublicKey, + #[error("The encryption key for this coinbase transaction wasn't provided")] + MissingEncryptionKey, + #[error("The sender offset key for this coinbase transaction wasn't provided")] + MissingSenderOffsetKey, #[error("The value encryption was not succeed")] ValueEncryptionFailed, #[error("An error occurred building the final transaction: `{0}`")] @@ -90,7 +98,10 @@ pub struct CoinbaseBuilder { fees: Option, spend_key_id: Option, script_key_id: Option, + encryption_key_id: Option, + sender_offset_key_id: Option, script: Option, + wallet_public_key: Option, covenant: Covenant, extra: Option>, } @@ -107,7 +118,10 @@ where TKeyManagerInterface: TransactionKeyManagerInterface fees: None, spend_key_id: None, script_key_id: None, + encryption_key_id: None, + sender_offset_key_id: None, script: None, + wallet_public_key: None, covenant: Covenant::default(), extra: None, } @@ -125,25 +139,45 @@ where TKeyManagerInterface: TransactionKeyManagerInterface self } - /// Provides the spend key for this transaction. This will usually be provided by a miner's wallet instance. + /// Provides the spend key ID for this transaction. This will usually be provided by a miner's wallet instance. pub fn with_spend_key_id(mut self, key: TariKeyId) -> Self { self.spend_key_id = Some(key); self } - /// Provides the script key for this transaction. This will usually be provided by a miner's wallet + /// Provides the script key ID for this transaction. This will usually be provided by a miner's wallet /// instance. pub fn with_script_key_id(mut self, key: TariKeyId) -> Self { self.script_key_id = Some(key); self } + /// Provides the encryption key ID for this transaction. This will usually be provided by a Diffie-Hellman shared + /// secret. + pub fn with_encryption_key_id(mut self, key: TariKeyId) -> Self { + self.encryption_key_id = Some(key); + self + } + + /// Provides the sender offset key ID for this transaction. This will usually be provided by a miner's wallet + /// instance. + pub fn with_sender_offset_key_id(mut self, key: TariKeyId) -> Self { + self.sender_offset_key_id = Some(key); + self + } + /// Provides the script for this transaction, usually by a miner's wallet instance. pub fn with_script(mut self, script: TariScript) -> Self { self.script = Some(script); self } + /// Provides the script for this transaction, usually by a miner's wallet instance. + pub fn with_wallet_public_key(mut self, wallet_public_key: PublicKey) -> Self { + self.wallet_public_key = Some(wallet_public_key); + self + } + /// Set the covenant for this transaction. pub fn with_covenant(mut self, covenant: Covenant) -> Self { self.covenant = covenant; @@ -181,6 +215,143 @@ where TKeyManagerInterface: TransactionKeyManagerInterface self, constants: &ConsensusConstants, block_reward: MicroMinotari, + ) -> Result<(Transaction, WalletOutput), CoinbaseBuildError> { + // gets tx details + let height = self.block_height.ok_or(CoinbaseBuildError::MissingBlockHeight)?; + let total_reward = block_reward + self.fees.ok_or(CoinbaseBuildError::MissingFees)?; + let spending_key_id = self.spend_key_id.ok_or(CoinbaseBuildError::MissingSpendKey)?; + let script_key_id = self.script_key_id.ok_or(CoinbaseBuildError::MissingScriptKey)?; + let encryption_key_id = self.encryption_key_id.ok_or(CoinbaseBuildError::MissingEncryptionKey)?; + let sender_offset_key_id = self + .sender_offset_key_id + .ok_or(CoinbaseBuildError::MissingSenderOffsetKey)?; + let covenant = self.covenant; + let script = self.script.ok_or(CoinbaseBuildError::MissingScript)?; + let wallet_public_key = self + .wallet_public_key + .ok_or(CoinbaseBuildError::MissingWalletPublicKey)?; + + let kernel_features = KernelFeatures::create_coinbase(); + let metadata = TransactionMetadata::new_with_features(0.into(), 0, kernel_features); + // generate kernel signature + let kernel_version = TransactionKernelVersion::get_current_version(); + let kernel_message = TransactionKernel::build_kernel_signature_message( + &kernel_version, + metadata.fee, + metadata.lock_height, + &metadata.kernel_features, + &metadata.burn_commitment, + ); + let (public_nonce_id, public_nonce) = self + .key_manager + .get_next_key(TransactionKeyManagerBranch::KernelNonce.get_branch_key()) + .await?; + + let public_spend_key = self.key_manager.get_public_key_at_key_id(&spending_key_id).await?; + + let kernel_signature = self + .key_manager + .get_partial_txo_kernel_signature( + &spending_key_id, + &public_nonce_id, + &public_nonce, + &public_spend_key, + &kernel_version, + &kernel_message, + &metadata.kernel_features, + TxoStage::Output, + ) + .await?; + + let excess = Commitment::from_public_key(&public_spend_key); + // generate tx details + let value: u64 = total_reward.into(); + let output_features = OutputFeatures::create_coinbase(height + constants.coinbase_min_maturity(), self.extra); + let encrypted_data = self + .key_manager + .encrypt_data_for_recovery(&spending_key_id, Some(&encryption_key_id), total_reward.into()) + .await?; + let minimum_value_promise = MicroMinotari::zero(); + + let output_version = TransactionOutputVersion::get_current_version(); + let metadata_message = TransactionOutput::metadata_signature_message_from_parts( + &output_version, + &script, + &output_features, + &covenant, + &encrypted_data, + &minimum_value_promise, + ); + + let sender_offset_public_key = self.key_manager.get_public_key_at_key_id(&sender_offset_key_id).await?; + + let metadata_sig = self + .key_manager + .get_metadata_signature( + &spending_key_id, + &value.into(), + &sender_offset_key_id, + &output_version, + &metadata_message, + output_features.range_proof_type, + ) + .await?; + + let wallet_output = WalletOutput::new( + output_version, + total_reward, + spending_key_id, + output_features, + script, + inputs!(wallet_public_key), + script_key_id, + sender_offset_public_key, + metadata_sig, + 0, + covenant, + encrypted_data, + minimum_value_promise, + &self.key_manager, + ) + .await?; + let output = wallet_output + .to_transaction_output(&self.key_manager) + .await + .map_err(|e| CoinbaseBuildError::BuildError(e.to_string()))?; + let kernel = KernelBuilder::new() + .with_fee(0 * uT) + .with_features(kernel_features) + .with_lock_height(0) + .with_excess(&excess) + .with_signature(kernel_signature) + .build() + .map_err(|e| CoinbaseBuildError::BuildError(e.to_string()))?; + + let mut builder = TransactionBuilder::new(); + builder + .add_output(output) + // A coinbase must have 0 offset or the reward balance check will fail. + .add_offset(PrivateKey::default()) + // Coinbase has no script offset https://rfc.tari.com/RFC-0201_TariScript.html#script-offset + .add_script_offset(PrivateKey::default()) + .with_reward(total_reward) + .with_kernel(kernel); + let tx = builder + .build() + .map_err(|e| CoinbaseBuildError::BuildError(e.to_string()))?; + Ok((tx, wallet_output)) + } + + /// Try and construct a Coinbase Transaction while specifying the block reward. The other parameters (keys, nonces + /// etc.) are provided by the caller. Other data is automatically set: Coinbase transactions have an offset of + /// zero, no fees, the `COINBASE_OUTPUT` flags are set on the output and kernel, and the maturity schedule is + /// set from the consensus rules. + #[allow(clippy::too_many_lines)] + #[allow(clippy::erasing_op)] // This is for 0 * uT + pub async fn build_with_reward_( + self, + constants: &ConsensusConstants, + block_reward: MicroMinotari, ) -> Result<(Transaction, WalletOutput), CoinbaseBuildError> { // gets tx details let height = self.block_height.ok_or(CoinbaseBuildError::MissingBlockHeight)?; diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index 12cd02b847..48bf8e0a05 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -36,6 +36,7 @@ use tari_core::{ SenderTransactionProtocol, }, }; +use tari_key_manager::key_manager_service::KeyId; use tari_script::TariScript; use tari_service_framework::reply_channel::SenderService; use tari_utilities::hex::Hex; @@ -67,6 +68,10 @@ pub enum OutputManagerRequest { fees: MicroMinotari, block_height: u64, extra: Vec, + script: TariScript, + spending_key_id: KeyId, + encryption_key: KeyId, + sender_offset_private_key_id: KeyId, }, ConfirmPendingTransaction(TxId), PrepareToSendTransaction { @@ -445,6 +450,38 @@ impl OutputManagerHandle { fees: MicroMinotari, block_height: u64, extra: Vec, + script: TariScript, + spending_key_id: KeyId, + encryption_key: KeyId, + sender_offset_private_key_id: KeyId, + ) -> Result { + match self + .handle + .call(OutputManagerRequest::GetCoinbaseTransaction { + tx_id, + reward, + fees, + block_height, + extra, + script, + spending_key_id, + encryption_key, + sender_offset_private_key_id, + }) + .await?? + { + OutputManagerResponse::CoinbaseTransaction(tx) => Ok(tx), + _ => Err(OutputManagerError::UnexpectedApiResponse), + } + } + + pub async fn _get_coinbase_transaction( + &mut self, + tx_id: TxId, + reward: MicroMinotari, + fees: MicroMinotari, + block_height: u64, + extra: Vec, ) -> Result { match self .handle @@ -454,6 +491,10 @@ impl OutputManagerHandle { fees, block_height, extra, + script: TariScript::default(), + spending_key_id: KeyId::default(), + encryption_key: KeyId::default(), + sender_offset_private_key_id: KeyId::default(), }) .await?? { diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 3de89bcaa6..8d89870b85 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -35,7 +35,10 @@ use tari_core::{ borsh::SerializedSize, consensus::ConsensusConstants, covenants::Covenant, - one_sided::{shared_secret_to_output_encryption_key, stealth_address_script_spending_key}, + one_sided::{ + shared_secret_to_output_encryption_key, + stealth_address_script_spending_key, + }, proto::base_node::FetchMatchingUtxos, transactions::{ fee::Fee, @@ -59,7 +62,8 @@ use tari_core::{ SenderTransactionProtocol, }, }; -use tari_crypto::keys::SecretKey; +use tari_crypto::keys::{PublicKey as PK, SecretKey}; +use tari_key_manager::key_manager_service::KeyId; use tari_script::{inputs, script, ExecutionStack, Opcode, TariScript}; use tari_service_framework::reply_channel; use tari_shutdown::ShutdownSignal; @@ -243,8 +247,22 @@ where fees, block_height, extra, + script, + spending_key_id, + encryption_key, + sender_offset_private_key_id, } => self - .get_coinbase_transaction(tx_id, reward, fees, block_height, extra) + .get_coinbase_transaction( + tx_id, + reward, + fees, + block_height, + extra, + script, + spending_key_id, + encryption_key, + sender_offset_private_key_id, + ) .await .map(OutputManagerResponse::CoinbaseTransaction), OutputManagerRequest::PrepareToSendTransaction { @@ -1011,6 +1029,68 @@ where fees: MicroMinotari, block_height: u64, extra: Vec, + script: TariScript, + spending_key_id: KeyId, + encryption_key_id: KeyId, + sender_offset_key_id: KeyId, + ) -> Result { + debug!( + target: LOG_TARGET, + "Building coinbase transaction for block_height {} with TxId: {}", block_height, tx_id + ); + + let (tx, wallet_output) = CoinbaseBuilder::new(self.resources.key_manager.clone()) + .with_block_height(block_height) + .with_fees(fees) + .with_spend_key_id(spending_key_id) + .with_encryption_key_id(encryption_key_id) + .with_sender_offset_key_id(sender_offset_key_id) + .with_script_key_id(self.resources.wallet_identity.wallet_node_key_id.clone()) + .with_script(script) + .with_wallet_public_key(PublicKey::from_secret_key( + self.resources.wallet_identity.node_identity.secret_key(), + )) + .with_extra(extra) + .build_with_reward(&self.resources.consensus_constants, reward) + .await?; + + let output = DbWalletOutput::from_wallet_output( + wallet_output, + &self.resources.key_manager, + None, + OutputSource::Coinbase, + Some(tx_id), + None, + ) + .await?; + + // If there is no existing output available, we store the one we produced. + match self.resources.db.fetch_by_commitment(output.commitment.clone()) { + Ok(_) => {}, + Err(OutputManagerStorageError::ValueNotFound) => { + self.resources + .db + .add_output_to_be_received(tx_id, output, Some(block_height))?; + + self.confirm_encumberance(tx_id)?; + }, + Err(e) => return Err(e.into()), + }; + + Ok(tx) + } + + /// Request a Coinbase transaction for a specific block height. All existing pending transactions with + /// the corresponding output hash will be cancelled. + /// The key will be derived from the coinbase specific keychain using the blockheight as an index. The coinbase + /// keychain is based on the wallets master_key and the "coinbase" branch. + async fn _get_coinbase_transaction( + &mut self, + tx_id: TxId, + reward: MicroMinotari, + fees: MicroMinotari, + block_height: u64, + extra: Vec, ) -> Result { debug!( target: LOG_TARGET, diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 9f8c127916..f03d7ffb24 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -148,6 +148,8 @@ pub enum TransactionServiceRequest { SetLowPowerMode, SetNormalPowerMode, GenerateCoinbaseTransaction { + wallet_payment_address: TariAddress, + stealth_payment: bool, reward: MicroMinotari, fees: MicroMinotari, block_height: u64, @@ -847,6 +849,8 @@ impl TransactionServiceHandle { pub async fn generate_coinbase_transaction( &mut self, + wallet_payment_address: TariAddress, + stealth_payment: bool, reward: MicroMinotari, fees: MicroMinotari, block_height: u64, @@ -855,6 +859,8 @@ impl TransactionServiceHandle { match self .handle .call(TransactionServiceRequest::GenerateCoinbaseTransaction { + wallet_payment_address, + stealth_payment, reward, fees, block_height, diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index ec74eb53ab..fd01d02e52 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -53,7 +53,7 @@ use tari_core::{ }, proto::base_node as base_node_proto, transactions::{ - key_manager::TransactionKeyManagerInterface, + key_manager::{TransactionKeyManagerBranch, TransactionKeyManagerInterface}, tari_amount::MicroMinotari, transaction_components::{ CodeTemplateRegistration, @@ -801,12 +801,14 @@ where .submit_transaction_to_self(transaction_broadcast_join_handles, tx_id, tx, fee, amount, message) .map(|_| TransactionServiceResponse::TransactionSubmitted), TransactionServiceRequest::GenerateCoinbaseTransaction { + wallet_payment_address, + stealth_payment, reward, fees, block_height, extra, } => self - .generate_coinbase_transaction(reward, fees, block_height, extra) + .generate_coinbase_transaction(wallet_payment_address, stealth_payment, reward, fees, block_height, extra) .await .map(|tx| TransactionServiceResponse::CoinbaseTransactionGenerated(Box::new(tx))), TransactionServiceRequest::SetLowPowerMode => { @@ -2920,6 +2922,126 @@ where } async fn generate_coinbase_transaction( + &mut self, + wallet_payment_address: TariAddress, + stealth_payment: bool, + reward: MicroMinotari, + fees: MicroMinotari, + block_height: u64, + extra: Vec, + ) -> Result { + let amount = reward + fees; + + // first check if we already have a coinbase tx for this height and amount + let find_result = self + .db + .find_coinbase_transaction_at_block_height(block_height, amount)?; + + let mut completed_transaction = None; + if let Some(tx) = find_result { + if let Some(coinbase) = tx.transaction.body.outputs().first() { + if coinbase.features.coinbase_extra == extra { + completed_transaction = Some(tx.transaction); + } + } + }; + if completed_transaction.is_none() { + // otherwise create a new coinbase tx + let tx_id = TxId::new_random(); + + let (sender_offset_key_id, _) = self + .resources + .transaction_key_manager_service + .get_next_key(TransactionKeyManagerBranch::SenderOffset.get_branch_key()) + .await?; + let shared_secret = self + .resources + .transaction_key_manager_service + .get_diffie_hellman_shared_secret(&sender_offset_key_id, wallet_payment_address.public_key()) + .await?; + let spending_key = shared_secret_to_output_spending_key(&shared_secret) + .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; + + let encryption_private_key = shared_secret_to_output_encryption_key(&shared_secret)?; + let encryption_key_id = self + .resources + .transaction_key_manager_service + .import_key(encryption_private_key) + .await?; + + let spending_key_id = self + .resources + .transaction_key_manager_service + .import_key(spending_key) + .await?; + + let script = if stealth_payment { + let (nonce_private_key, nonce_public_key) = PublicKey::random_keypair(&mut OsRng); + let c = diffie_hellman_stealth_domain_hasher(&nonce_private_key, wallet_payment_address.public_key()); + let script_spending_key = stealth_address_script_spending_key(&c, wallet_payment_address.public_key()); + stealth_payment_script(&nonce_public_key, &script_spending_key) + } else { + one_sided_payment_script(wallet_payment_address.public_key()) + }; + + let tx = self + .resources + .output_manager_service + .get_coinbase_transaction( + tx_id, + reward, + fees, + block_height, + extra, + script, + spending_key_id, + encryption_key_id, + sender_offset_key_id, + ) + .await?; + self.db.insert_completed_transaction( + tx_id, + CompletedTransaction::new( + tx_id, + self.resources.wallet_identity.address.clone(), + self.resources.wallet_identity.address.clone(), + amount, + MicroMinotari::from(0), + tx.clone(), + TransactionStatus::Coinbase, + format!("Coinbase Transaction for Block #{}", block_height), + Utc::now().naive_utc(), + TransactionDirection::Inbound, + Some(block_height), + None, + None, + ), + )?; + + let _size = self + .resources + .event_publisher + .send(Arc::new(TransactionEvent::ReceivedFinalizedTransaction(tx_id))) + .map_err(|e| { + trace!( + target: LOG_TARGET, + "Error sending event because there are no subscribers: {:?}", + e + ); + e + }); + + info!( + target: LOG_TARGET, + "Coinbase transaction (TxId: {}) for Block Height: {} added", tx_id, block_height + ); + completed_transaction = Some(tx); + }; + + Ok(completed_transaction.unwrap()) + } + + async fn _generate_coinbase_transaction( &mut self, reward: MicroMinotari, fees: MicroMinotari, @@ -2947,7 +3069,7 @@ where let tx = self .resources .output_manager_service - .get_coinbase_transaction(tx_id, reward, fees, block_height, extra) + ._get_coinbase_transaction(tx_id, reward, fees, block_height, extra) .await?; self.db.insert_completed_transaction( tx_id, diff --git a/base_layer/wallet/tests/output_manager_service_tests/service.rs b/base_layer/wallet/tests/output_manager_service_tests/service.rs index a25bdcb42c..d754d516b5 100644 --- a/base_layer/wallet/tests/output_manager_service_tests/service.rs +++ b/base_layer/wallet/tests/output_manager_service_tests/service.rs @@ -43,6 +43,7 @@ use minotari_wallet::{ util::wallet_identity::WalletIdentity, }; use rand::{rngs::OsRng, RngCore}; +use tari_crypto::keys::PublicKey as PK; use tari_common::configuration::Network; use tari_common_types::{ transaction::TxId, @@ -77,7 +78,7 @@ use tari_core::{ }, }; use tari_key_manager::key_manager_service::KeyManagerInterface; -use tari_script::{inputs, script, TariScript}; +use tari_script::{inputs, one_sided_payment_script, script, stealth_payment_script, TariScript}; use tari_service_framework::reply_channel; use tari_shutdown::Shutdown; use tokio::{ @@ -85,6 +86,9 @@ use tokio::{ task, time::sleep, }; +use tari_common_types::tari_address::TariAddress; +use tari_core::one_sided::{diffie_hellman_stealth_domain_hasher, shared_secret_to_output_encryption_key, shared_secret_to_output_spending_key, stealth_address_script_spending_key}; +use tari_core::transactions::key_manager::TariKeyId; use crate::support::{ base_node_service_mock::MockBaseNodeService, @@ -1257,6 +1261,39 @@ async fn it_handles_large_coin_splits() { assert_eq!(coin_split_tx.body.outputs().len(), split_count + 1); } +async fn get_coinbase_inputs(oms: &TestOmsService, wallet_payment_address: &TariAddress, stealth_payment: bool) -> (TariScript, TariKeyId, TariKeyId, TariKeyId) { + let (sender_offset_key_id, _) = oms + .key_manager_handle + .get_next_key(TransactionKeyManagerBranch::SenderOffset.get_branch_key()) + .await.unwrap(); + let shared_secret = oms + .key_manager_handle + .get_diffie_hellman_shared_secret(&sender_offset_key_id, wallet_payment_address.public_key()) + .await.unwrap(); + let spending_key = shared_secret_to_output_spending_key(&shared_secret).unwrap(); + + let encryption_private_key = shared_secret_to_output_encryption_key(&shared_secret).unwrap(); + let encryption_key_id = oms + .key_manager_handle + .import_key(encryption_private_key) + .await.unwrap(); + + let spending_key_id = oms + .key_manager_handle + .import_key(spending_key) + .await.unwrap(); + + let script = if stealth_payment { + let (nonce_private_key, nonce_public_key) = PublicKey::random_keypair(&mut OsRng); + let c = diffie_hellman_stealth_domain_hasher(&nonce_private_key, wallet_payment_address.public_key()); + let script_spending_key = stealth_address_script_spending_key(&c, wallet_payment_address.public_key()); + stealth_payment_script(&nonce_public_key, &script_spending_key) + } else { + one_sided_payment_script(wallet_payment_address.public_key()) + }; + (script, spending_key_id, encryption_key_id, sender_offset_key_id) +} + #[tokio::test] async fn handle_coinbase_with_bulletproofs_rewinding() { let (connection, _tempdir) = get_temp_sqlite_database_connection(); @@ -1271,9 +1308,10 @@ async fn handle_coinbase_with_bulletproofs_rewinding() { let fees3 = MicroMinotari::from(500); let value3 = reward3 + fees3; + let (script, spending_key_id, encryption_key_id, sender_offset_key_id) = get_coinbase_inputs(&oms, &TariAddress::default(), false).await; let _transaction = oms .output_manager_handle - .get_coinbase_transaction(1u64.into(), reward1, fees1, 1, b"test".to_vec()) + .get_coinbase_transaction(1u64.into(), reward1, fees1, 1, b"test".to_vec(), script.clone(), spending_key_id.clone(), encryption_key_id.clone(), sender_offset_key_id.clone()) .await .unwrap(); assert_eq!(oms.output_manager_handle.get_unspent_outputs().await.unwrap().len(), 0); @@ -1289,7 +1327,7 @@ async fn handle_coinbase_with_bulletproofs_rewinding() { let _tx2 = oms .output_manager_handle - .get_coinbase_transaction(2u64.into(), reward2, fees2, 1, b"test".to_vec()) + .get_coinbase_transaction(2u64.into(), reward2, fees2, 1, b"test".to_vec(), script.clone(), spending_key_id.clone(), encryption_key_id.clone(), sender_offset_key_id.clone()) .await .unwrap(); assert_eq!(oms.output_manager_handle.get_unspent_outputs().await.unwrap().len(), 0); @@ -1303,7 +1341,7 @@ async fn handle_coinbase_with_bulletproofs_rewinding() { ); let tx3 = oms .output_manager_handle - .get_coinbase_transaction(3u64.into(), reward3, fees3, 2, b"test".to_vec()) + .get_coinbase_transaction(3u64.into(), reward3, fees3, 2, b"test".to_vec(), script, spending_key_id, encryption_key_id, sender_offset_key_id) .await .unwrap(); assert_eq!(oms.output_manager_handle.get_unspent_outputs().await.unwrap().len(), 0); @@ -1329,18 +1367,18 @@ async fn handle_coinbase_with_bulletproofs_rewinding() { #[tokio::test] #[allow(clippy::too_many_lines)] async fn test_txo_validation() { - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone()); + let (db_connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(db_connection.clone()); let oms_db = backend.clone(); let mut oms = setup_output_manager_service(backend, true).await; // Now we add the connection - let mut connection = oms + let mut peer_connection = oms .mock_rpc_service .create_connection(oms.node_id.to_peer(), "t/bnwallet/1".into()) .await; oms.wallet_connectivity_mock - .set_base_node_wallet_rpc_client(connect_rpc_client(&mut connection).await); + .set_base_node_wallet_rpc_client(connect_rpc_client(&mut peer_connection).await); let output1_value = 1_000_000; let output1 = make_input( @@ -1481,7 +1519,9 @@ async fn test_txo_validation() { .get_recipient_transaction(sender_message) .await .unwrap(); + let (script, spending_key_id, encryption_key_id, sender_offset_key_id) = get_coinbase_inputs(&oms, &TariAddress::default(), false).await; + // The coinbase will not be detected in the responses, as it will go to a different wallet oms.output_manager_handle .get_coinbase_transaction( 6u64.into(), @@ -1489,23 +1529,19 @@ async fn test_txo_validation() { MicroMinotari::from(1_000_000), 2, b"test".to_vec(), + script, spending_key_id, encryption_key_id, sender_offset_key_id ) .await .unwrap(); let mut outputs = oms_db.fetch_pending_incoming_outputs().unwrap(); - assert_eq!(outputs.len(), 3); + assert_eq!(outputs.len(), 2); let o5_pos = outputs .iter() .position(|o| o.wallet_output.value == MicroMinotari::from(8_000_000)) .unwrap(); let output5 = outputs.remove(o5_pos); - let o6_pos = outputs - .iter() - .position(|o| o.wallet_output.value == MicroMinotari::from(16_000_000)) - .unwrap(); - let output6 = outputs.remove(o6_pos); let output4 = outputs[0].clone(); let output4_tx_output = output4 @@ -1518,11 +1554,6 @@ async fn test_txo_validation() { .to_transaction_output(&oms.key_manager_handle) .await .unwrap(); - let output6_tx_output = output6 - .wallet_output - .to_transaction_output(&oms.key_manager_handle) - .await - .unwrap(); let balance = oms.output_manager_handle.get_balance().await.unwrap(); @@ -1581,13 +1612,6 @@ async fn test_txo_validation() { output_hash: output5_tx_output.hash().to_vec(), mined_timestamp: 0, }, - UtxoQueryResponse { - output: Some(output6_tx_output.clone().try_into().unwrap()), - mined_at_height: 5, - mined_in_block: block5_header.hash().to_vec(), - output_hash: output6_tx_output.hash().to_vec(), - mined_timestamp: 0, - }, ]; let mut utxo_query_responses = UtxoQueryResponses { diff --git a/base_layer/wallet/tests/transaction_service_tests/service.rs b/base_layer/wallet/tests/transaction_service_tests/service.rs index 5f1bd51ae2..56ef360443 100644 --- a/base_layer/wallet/tests/transaction_service_tests/service.rs +++ b/base_layer/wallet/tests/transaction_service_tests/service.rs @@ -3595,7 +3595,7 @@ async fn test_coinbase_transactions_rejection_same_hash_but_accept_on_same_heigh // Create a coinbase Txn at the first block height let _tx1 = alice_ts_interface .transaction_service_handle - .generate_coinbase_transaction(reward1, fees1, block_height_a, b"test".to_vec()) + .generate_coinbase_transaction(TariAddress::default(), true, reward1, fees1, block_height_a, b"test".to_vec()) .await .unwrap(); let transactions = alice_ts_interface @@ -3623,7 +3623,7 @@ async fn test_coinbase_transactions_rejection_same_hash_but_accept_on_same_heigh // the previous one should be cancelled let _tx1b = alice_ts_interface .transaction_service_handle - .generate_coinbase_transaction(reward1, fees1, block_height_a, b"test".to_vec()) + .generate_coinbase_transaction(TariAddress::default(), true, reward1, fees1, block_height_a, b"test".to_vec()) .await .unwrap(); let transactions = alice_ts_interface @@ -3650,7 +3650,7 @@ async fn test_coinbase_transactions_rejection_same_hash_but_accept_on_same_heigh // Create another coinbase Txn at the same block height; the previous one should not be cancelled let _tx2 = alice_ts_interface .transaction_service_handle - .generate_coinbase_transaction(reward2, fees2, block_height_a, b"test".to_vec()) + .generate_coinbase_transaction(TariAddress::default(), true, reward2, fees2, block_height_a, b"test".to_vec()) .await .unwrap(); let transactions = alice_ts_interface @@ -3677,7 +3677,7 @@ async fn test_coinbase_transactions_rejection_same_hash_but_accept_on_same_heigh // Create a third coinbase Txn at the second block height; all the three should be valid let _tx3 = alice_ts_interface .transaction_service_handle - .generate_coinbase_transaction(reward3, fees3, block_height_b, b"test".to_vec()) + .generate_coinbase_transaction(TariAddress::default(), true, reward3, fees3, block_height_b, b"test".to_vec()) .await .unwrap(); let transactions = alice_ts_interface @@ -3733,7 +3733,7 @@ async fn test_coinbase_generation_and_monitoring() { // Create a coinbase Txn at the first block height let _tx1 = alice_ts_interface .transaction_service_handle - .generate_coinbase_transaction(reward1, fees1, block_height_a, b"test".to_vec()) + .generate_coinbase_transaction(TariAddress::default(), true, reward1, fees1, block_height_a, b"test".to_vec()) .await .unwrap(); let transactions = alice_ts_interface @@ -3760,7 +3760,7 @@ async fn test_coinbase_generation_and_monitoring() { // Create another coinbase Txn at the next block height let _tx2 = alice_ts_interface .transaction_service_handle - .generate_coinbase_transaction(reward2, fees2, block_height_b, b"test".to_vec()) + .generate_coinbase_transaction(TariAddress::default(), true, reward2, fees2, block_height_b, b"test".to_vec()) .await .unwrap(); let transactions = alice_ts_interface @@ -3787,7 +3787,7 @@ async fn test_coinbase_generation_and_monitoring() { // Take out a second one at the second height which should not overwrite the initial one let _tx2b = alice_ts_interface .transaction_service_handle - .generate_coinbase_transaction(reward2, fees2b, block_height_b, b"test".to_vec()) + .generate_coinbase_transaction(TariAddress::default(), true, reward2, fees2b, block_height_b, b"test".to_vec()) .await .unwrap(); let transactions = alice_ts_interface @@ -3976,7 +3976,7 @@ async fn test_coinbase_abandoned() { let tx1 = alice_ts_interface .transaction_service_handle - .generate_coinbase_transaction(reward1, fees1, block_height_a, b"test".to_vec()) + .generate_coinbase_transaction(TariAddress::default(), true, reward1, fees1, block_height_a, b"test".to_vec()) .await .unwrap(); let transactions = alice_ts_interface @@ -4102,7 +4102,7 @@ async fn test_coinbase_abandoned() { let tx2 = alice_ts_interface .transaction_service_handle - .generate_coinbase_transaction(reward2, fees2, block_height_b, b"test".to_vec()) + .generate_coinbase_transaction(TariAddress::default(), true, reward2, fees2, block_height_b, b"test".to_vec()) .await .unwrap(); let transactions = alice_ts_interface @@ -4425,13 +4425,13 @@ async fn test_coinbase_transaction_reused_for_same_height() { // a requested coinbase transaction for the same height and amount should be the same let tx1 = ts_interface .transaction_service_handle - .generate_coinbase_transaction(reward1, fees1, blockheight1, b"test".to_vec()) + .generate_coinbase_transaction(TariAddress::default(), true, reward1, fees1, blockheight1, b"test".to_vec()) .await .unwrap(); let tx2 = ts_interface .transaction_service_handle - .generate_coinbase_transaction(reward1, fees1, blockheight1, b"test".to_vec()) + .generate_coinbase_transaction(TariAddress::default(), true, reward1, fees1, blockheight1, b"test".to_vec()) .await .unwrap(); @@ -4461,7 +4461,7 @@ async fn test_coinbase_transaction_reused_for_same_height() { // a requested coinbase transaction for the same height but new amount should be different let tx3 = ts_interface .transaction_service_handle - .generate_coinbase_transaction(reward2, fees2, blockheight1, b"test".to_vec()) + .generate_coinbase_transaction(TariAddress::default(), true, reward2, fees2, blockheight1, b"test".to_vec()) .await .unwrap(); @@ -4490,7 +4490,7 @@ async fn test_coinbase_transaction_reused_for_same_height() { // a requested coinbase transaction for a new height should be different let tx_height2 = ts_interface .transaction_service_handle - .generate_coinbase_transaction(reward2, fees2, blockheight2, b"test".to_vec()) + .generate_coinbase_transaction(TariAddress::default(), true, reward2, fees2, blockheight2, b"test".to_vec()) .await .unwrap(); diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index a0d6104a95..344ce874e7 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -18,6 +18,7 @@ tari_common = { path = "../common" } tari_common_types = { path = "../base_layer/common_types" } tari_comms = { path = "../comms/core" } tari_comms_dht = { path = "../comms/dht" } +tari_key_manager = { path = "../base_layer/key_manager", features = ["key_manager_service"] } minotari_console_wallet = { path = "../applications/minotari_console_wallet" } tari_contacts = { path = "../base_layer/contacts" } tari_core = { path = "../base_layer/core" } diff --git a/integration_tests/src/miner.rs b/integration_tests/src/miner.rs index 73494a7b0c..78c173ab8d 100644 --- a/integration_tests/src/miner.rs +++ b/integration_tests/src/miner.rs @@ -41,22 +41,36 @@ use minotari_app_grpc::{ use minotari_app_utilities::common_cli_args::CommonCliArgs; use minotari_miner::{run_miner, Cli}; use minotari_node_grpc_client::BaseNodeGrpcClient; +use rand::rngs::OsRng; use tari_common::configuration::Network; -use tari_common_types::grpc_authentication::GrpcAuthentication; +use tari_common_types::{ + grpc_authentication::GrpcAuthentication, + types::{PrivateKey, PublicKey}, +}; use tari_core::{ consensus::ConsensusManager, + one_sided::{ + diffie_hellman_stealth_domain_hasher, + shared_secret_to_output_encryption_key, + shared_secret_to_output_spending_key, + stealth_address_script_spending_key, + }, transactions::{ - key_manager::TransactionKeyManagerInterface, + key_manager::{TransactionKeyManagerBranch, TransactionKeyManagerInterface}, test_helpers::TestKeyManager, transaction_components::WalletOutput, CoinbaseBuilder, }, }; +use tari_crypto::keys::PublicKey as PK; +use tari_script::{one_sided_payment_script, stealth_payment_script}; +use tari_utilities::ByteArray; use tonic::{ codegen::InterceptedService, transport::{Channel, Endpoint}, }; - +use tari_common_types::tari_address::TariAddress; +use tari_key_manager::key_manager_service::KeyManagerInterface; use crate::TariWorld; type BaseNodeClient = BaseNodeGrpcClient; @@ -131,12 +145,12 @@ impl MinerProcess { } #[allow(dead_code)] -pub async fn mine_blocks(world: &mut TariWorld, miner_name: String, num_blocks: u64) { +pub async fn mine_blocks(world: &mut TariWorld, miner_name: String, num_blocks: u64, wallet_payment_address: &PublicKey) { let mut base_client = create_base_node_client(world, &miner_name).await; let mut wallet_client = create_wallet_client(world, &miner_name).await; for _ in 0..num_blocks { - mine_block(&mut base_client, &mut wallet_client).await; + mine_block(&mut base_client, &mut wallet_client, wallet_payment_address).await; tokio::time::sleep(Duration::from_millis(100)).await; } @@ -179,8 +193,8 @@ async fn create_wallet_client(world: &TariWorld, miner_name: &String) -> WalletG ) } -pub async fn mine_block(base_client: &mut BaseNodeClient, wallet_client: &mut WalletGrpcClient) { - let block_template = create_block_template_with_coinbase(base_client, wallet_client).await; +pub async fn mine_block(base_client: &mut BaseNodeClient, wallet_client: &mut WalletGrpcClient, wallet_payment_address: &PublicKey) { + let block_template = create_block_template_with_coinbase(base_client, wallet_client, wallet_payment_address).await; // Ask the base node for a valid block using the template let block_result = base_client @@ -224,6 +238,7 @@ async fn mine_block_without_wallet_with_template(base_client: &mut BaseNodeClien async fn create_block_template_with_coinbase( base_client: &mut BaseNodeClient, wallet_client: &mut WalletGrpcClient, + wallet_payment_address: &PublicKey, ) -> NewBlockTemplate { // get the block template from the base node let template_req = NewBlockTemplateRequest { @@ -242,7 +257,8 @@ async fn create_block_template_with_coinbase( let mut block_template = template_res.new_block_template.clone().unwrap(); // add the coinbase outputs and kernels to the block template - let (output, kernel) = get_coinbase_outputs_and_kernels(wallet_client, template_res).await; + let (output, kernel) = get_coinbase_outputs_and_kernels(wallet_client, template_res, + wallet_payment_address).await; let body = block_template.body.as_mut().unwrap(); body.outputs.push(output); @@ -273,7 +289,11 @@ async fn create_block_template_with_coinbase_without_wallet( // let mut block_template = template_res.new_block_template.clone().unwrap(); // add the coinbase outputs and kernels to the block template - let (output, kernel, wallet_output) = get_coinbase_without_wallet_client(template_res.clone(), key_manager).await; + let (output, kernel, wallet_output) = get_coinbase_without_wallet_client( + template_res.clone(), + key_manager, + ) + .await; // let body = block_template.body.as_mut().unwrap(); template_res @@ -301,8 +321,9 @@ async fn create_block_template_with_coinbase_without_wallet( async fn get_coinbase_outputs_and_kernels( wallet_client: &mut WalletGrpcClient, template_res: NewBlockTemplateResponse, + wallet_payment_address: &PublicKey, ) -> (TransactionOutput, TransactionKernel) { - let coinbase_req = coinbase_request(&template_res); + let coinbase_req = coinbase_request(&template_res, wallet_payment_address); let coinbase_res = wallet_client.get_coinbase(coinbase_req).await.unwrap().into_inner(); extract_outputs_and_kernels(coinbase_res) } @@ -311,20 +332,50 @@ async fn get_coinbase_without_wallet_client( template_res: NewBlockTemplateResponse, key_manager: &TestKeyManager, ) -> (TransactionOutput, TransactionKernel, WalletOutput) { - let coinbase_req = coinbase_request(&template_res); - generate_coinbase(coinbase_req, key_manager).await + let wallet_payment_address = PublicKey::default(); + let wallet_private_key = PrivateKey::default(); + let coinbase_req = coinbase_request(&template_res, &wallet_payment_address); + generate_coinbase(coinbase_req, key_manager, &wallet_private_key).await } async fn generate_coinbase( coinbase_req: GetCoinbaseRequest, key_manager: &TestKeyManager, + wallet_private_key: &PrivateKey, ) -> (TransactionOutput, TransactionKernel, WalletOutput) { let reward = coinbase_req.reward; let height = coinbase_req.height; let fee = coinbase_req.fee; let extra = coinbase_req.extra; + let wallet_payment_address = TariAddress::from_bytes(&coinbase_req.wallet_payment_address).unwrap(); + let stealth_payment = coinbase_req.stealth_payment; + + let (sender_offset_key_id, _) = key_manager + .get_next_key(TransactionKeyManagerBranch::SenderOffset.get_branch_key()) + .await + .unwrap(); + let shared_secret = key_manager + .get_diffie_hellman_shared_secret(&sender_offset_key_id, wallet_payment_address.public_key()) + .await + .unwrap(); + let spending_key = shared_secret_to_output_spending_key(&shared_secret).unwrap(); + + let encryption_private_key = shared_secret_to_output_encryption_key(&shared_secret).unwrap(); + let encryption_key_id = key_manager.import_key(encryption_private_key).await.unwrap(); + + let spending_key_id = key_manager.import_key(spending_key).await.unwrap(); + + let script = if stealth_payment { + let (nonce_private_key, nonce_public_key) = PublicKey::random_keypair(&mut OsRng); + let c = diffie_hellman_stealth_domain_hasher(&nonce_private_key, wallet_payment_address.public_key()); + let script_spending_key = stealth_address_script_spending_key(&c, wallet_payment_address.public_key()); + stealth_payment_script(&nonce_public_key, &script_spending_key) + } else { + one_sided_payment_script(wallet_payment_address.public_key()) + }; - let (spending_key_id, _, script_key_id, _) = key_manager.get_next_spend_and_script_key_ids().await.unwrap(); + let wallet_public_key = PublicKey::from_secret_key(wallet_private_key); + let wallet_node_key_id = key_manager.import_key(wallet_private_key.clone()).await.unwrap(); let consensus_manager = ConsensusManager::builder(Network::LocalNet).build().unwrap(); let consensus_constants = consensus_manager.consensus_constants(height); @@ -333,7 +384,11 @@ async fn generate_coinbase( .with_block_height(height) .with_fees(fee.into()) .with_spend_key_id(spending_key_id) - .with_script_key_id(script_key_id) + .with_encryption_key_id(encryption_key_id) + .with_sender_offset_key_id(sender_offset_key_id) + .with_script_key_id(wallet_node_key_id.clone()) + .with_script(script) + .with_wallet_public_key(wallet_public_key) .with_extra(extra) .build_with_reward(consensus_constants, reward.into()) .await @@ -345,17 +400,23 @@ async fn generate_coinbase( (tx_out.try_into().unwrap(), tx_krnl.into(), ubutxo) } -fn coinbase_request(template_response: &NewBlockTemplateResponse) -> GetCoinbaseRequest { +fn coinbase_request( + template_response: &NewBlockTemplateResponse, + wallet_payment_address: &PublicKey, +) -> GetCoinbaseRequest { let template = template_response.new_block_template.as_ref().unwrap(); let miner_data = template_response.miner_data.as_ref().unwrap(); let fee = miner_data.total_fees; let reward = miner_data.reward; let height = template.header.as_ref().unwrap().height; + let wallet_payment_address = wallet_payment_address.as_bytes().to_vec(); GetCoinbaseRequest { reward, fee, height, extra: vec![], + wallet_payment_address, + stealth_payment: true, } } diff --git a/integration_tests/src/world.rs b/integration_tests/src/world.rs index 1ff9058588..8ff908b77c 100644 --- a/integration_tests/src/world.rs +++ b/integration_tests/src/world.rs @@ -79,6 +79,7 @@ pub struct TariWorld { pub merge_mining_proxies: IndexMap, pub transactions: IndexMap, pub wallet_addresses: IndexMap, // values are strings representing tari addresses + pub wallet_payment_addresses: IndexMap, // values are strings representing tari addresses pub utxos: IndexMap, pub output_hash: Option, pub pre_image: Option, @@ -110,6 +111,7 @@ impl Default for TariWorld { merge_mining_proxies: Default::default(), transactions: Default::default(), wallet_addresses: Default::default(), + wallet_payment_addresses: Default::default(), utxos: Default::default(), output_hash: None, pre_image: None, @@ -136,6 +138,7 @@ impl Debug for TariWorld { .field("chat_clients", &self.chat_clients.keys()) .field("transactions", &self.transactions) .field("wallet_addresses", &self.wallet_addresses) + .field("wallet_payment_addresses", &self.wallet_payment_addresses) .field("utxos", &self.utxos) .field("output_hash", &self.output_hash) .field("pre_image", &self.pre_image) @@ -199,6 +202,30 @@ impl TariWorld { } } + pub async fn get_wallet_payment_address>(&self, name: &S) -> anyhow::Result { + if let Some(address) = self.wallet_payment_addresses.get(name.as_ref()) { + return Ok(address.clone()); + } + match self.get_wallet_client(name).await { + Ok(wallet) => { + let mut wallet = wallet; + + Ok(wallet + .get_address(minotari_wallet_grpc_client::grpc::Empty {}) + .await + .unwrap() + .into_inner() + .address + .to_hex()) + }, + Err(_) => { + let ffi_wallet = self.get_ffi_wallet(name).unwrap(); + + Ok(ffi_wallet.get_address().address().get_as_hex()) + }, + } + } + #[allow(dead_code)] pub async fn get_wallet_client>( &self, diff --git a/integration_tests/tests/steps/mining_steps.rs b/integration_tests/tests/steps/mining_steps.rs index bbc5d12f71..0005d5e32c 100644 --- a/integration_tests/tests/steps/mining_steps.rs +++ b/integration_tests/tests/steps/mining_steps.rs @@ -25,7 +25,8 @@ use std::{convert::TryFrom, time::Duration}; use cucumber::{given, then, when}; use minotari_app_grpc::tari_rpc::{self as grpc, GetTransactionInfoRequest}; use rand::Rng; -use tari_common_types::types::BlockHash; +use tari_utilities::hex::Hex; +use tari_common_types::types::{BlockHash, PublicKey}; use tari_core::blocks::Block; use tari_integration_tests::{ base_node_process::spawn_base_node, @@ -162,9 +163,12 @@ async fn while_mining_in_node_all_txs_in_wallet_are_mined_confirmed( world: &mut TariWorld, node: String, wallet: String, + wallet_payment: String, ) { let mut wallet_client = create_wallet_client(world, wallet.clone()).await.unwrap(); let wallet_address = world.get_wallet_address(&wallet).await.unwrap(); + let payment_address = world.get_wallet_payment_address(&wallet_payment).await.unwrap(); + let wallet_payment_address = PublicKey::from_hex(&payment_address).unwrap(); let wallet_tx_ids = world.wallet_tx_ids.get(&wallet_address).unwrap(); if wallet_tx_ids.is_empty() { @@ -200,7 +204,7 @@ async fn while_mining_in_node_all_txs_in_wallet_are_mined_confirmed( } println!("Mine a block for tx_id {} to have status Mined_Confirmed", tx_id); - mine_block(&mut node_client, &mut wallet_client).await; + mine_block(&mut node_client, &mut wallet_client, &wallet_payment_address).await; tokio::time::sleep(Duration::from_secs(5)).await; }