From 148e398ddd7c6a232f4246e80b6f938080163023 Mon Sep 17 00:00:00 2001 From: SW van Heerden Date: Wed, 27 Mar 2024 10:05:18 +0200 Subject: [PATCH] feat: new template with coinbase call (#6226) Description --- This adds a new grpc call to take a block template and add coinbases to it as desired. The callee is required to ensure that the coinbases amounts provided are correct and per consensus. Motivation and Context --- This is to allow external miners to create a block with 1 sided coinbase utxo's to be added without having to create them as this necessitates the inclusion of all Tari crypto libraries. How Has This Been Tested? --- New unit and cucumber tests --- Cargo.lock | 1 + .../minotari_app_grpc/proto/base_node.proto | 27 + applications/minotari_node/Cargo.toml | 3 + applications/minotari_node/src/config.rs | 2 + .../src/grpc/base_node_grpc_server.rs | 490 +++++++++++++++++- .../core/src/transactions/coinbase_builder.rs | 132 ++++- .../transaction_kernel.rs | 2 +- .../c_base_node_b_mining_allow_methods.toml | 2 + ..._base_node_b_non_mining_allow_methods.toml | 2 + integration_tests/src/base_node_process.rs | 2 + .../tests/features/BlockTemplate.feature | 7 + integration_tests/tests/steps/node_steps.rs | 157 +++++- 12 files changed, 819 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 321eb296ba..1e68eb558b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3353,6 +3353,7 @@ dependencies = [ "tari_core", "tari_crypto", "tari_features", + "tari_key_manager", "tari_libtor", "tari_metrics", "tari_p2p", diff --git a/applications/minotari_app_grpc/proto/base_node.proto b/applications/minotari_app_grpc/proto/base_node.proto index 57aabd60f8..84b7f07ada 100644 --- a/applications/minotari_app_grpc/proto/base_node.proto +++ b/applications/minotari_app_grpc/proto/base_node.proto @@ -57,6 +57,10 @@ service BaseNode { rpc GetNewBlockTemplate(NewBlockTemplateRequest) returns (NewBlockTemplateResponse); // Construct a new block from a provided template rpc GetNewBlock(NewBlockTemplate) returns (GetNewBlockResult); + // Construct a new block from a provided template + rpc GetNewBlockWithCoinbases(GetNewBlockWithCoinbasesRequest) returns (GetNewBlockResult); + // Construct a new block from a provided template + rpc GetNewBlockTemplateWithCoinbases(GetNewBlockTemplateWithCoinbasesRequest) returns (GetNewBlockResult); // Construct a new block and header blob from a provided template rpc GetNewBlockBlob(NewBlockTemplate) returns (GetNewBlockBlobResult); // Submit a new block for propagation @@ -182,6 +186,28 @@ message NewBlockTemplateRequest{ uint64 max_weight = 2; } +/// return type of NewBlockTemplateRequest +message GetNewBlockTemplateWithCoinbasesRequest{ + PowAlgo algo = 1; + //This field should be moved to optional once optional keyword is standard + uint64 max_weight = 2; + repeated NewBlockCoinbase coinbases = 3; +} + +/// request type of GetNewBlockWithCoinbasesRequest +message GetNewBlockWithCoinbasesRequest{ + NewBlockTemplate new_template = 1; + repeated NewBlockCoinbase coinbases = 2; +} + +message NewBlockCoinbase{ + string address = 1; + uint64 value = 2; + bool stealth_payment= 3; + bool revealed_value_proof= 4; + bytes coinbase_extra =5; +} + // Network difficulty response message NetworkDifficultyResponse { uint64 difficulty = 1; @@ -348,6 +374,7 @@ message GetNewBlockResult{ Block block = 2; bytes merge_mining_hash =3; bytes tari_unique_id =4; + MinerData miner_data = 5; } // This is the message that is returned for a miner after it asks for a new block. diff --git a/applications/minotari_node/Cargo.toml b/applications/minotari_node/Cargo.toml index d2ab1d56a1..76b66e9d5c 100644 --- a/applications/minotari_node/Cargo.toml +++ b/applications/minotari_node/Cargo.toml @@ -22,6 +22,9 @@ tari_storage = {path="../../infrastructure/storage"} tari_service_framework = { path = "../../base_layer/service_framework" } tari_shutdown = { path = "../../infrastructure/shutdown" } tari_utilities = { version = "0.7" } +tari_key_manager = { path = "../../base_layer/key_manager", features = [ + "key_manager_service", +], version = "1.0.0-pre.11a" } anyhow = "1.0.53" async-trait = "0.1.52" diff --git a/applications/minotari_node/src/config.rs b/applications/minotari_node/src/config.rs index aac19faacd..cb9435fc97 100644 --- a/applications/minotari_node/src/config.rs +++ b/applications/minotari_node/src/config.rs @@ -235,6 +235,8 @@ pub enum GrpcMethod { GetNetworkDifficulty, GetNewBlockTemplate, GetNewBlock, + GetNewBlockWithCoinbases, + GetNewBlockTemplateWithCoinbases, GetNewBlockBlob, SubmitBlock, SubmitBlockBlob, diff --git a/applications/minotari_node/src/grpc/base_node_grpc_server.rs b/applications/minotari_node/src/grpc/base_node_grpc_server.rs index 468bc0f537..8a6b83f0bb 100644 --- a/applications/minotari_node/src/grpc/base_node_grpc_server.rs +++ b/applications/minotari_node/src/grpc/base_node_grpc_server.rs @@ -34,7 +34,10 @@ use minotari_app_grpc::{ tari_rpc::{CalcType, Sorting}, }; use minotari_app_utilities::consts; -use tari_common_types::types::{Commitment, FixedHash, PublicKey, Signature}; +use tari_common_types::{ + tari_address::TariAddress, + types::{Commitment, FixedHash, PublicKey, Signature}, +}; use tari_comms::{Bytes, CommsNode}; use tari_core::{ base_node::{ @@ -49,8 +52,25 @@ use tari_core::{ iterators::NonOverlappingIntegerPairIter, mempool::{service::LocalMempoolService, TxStorageResponse}, proof_of_work::PowAlgorithm, - transactions::transaction_components::Transaction, + transactions::{ + generate_coinbase_with_wallet_output, + key_manager::{ + create_memory_db_key_manager, + TariKeyId, + TransactionKeyManagerBranch, + TransactionKeyManagerInterface, + TxoStage, + }, + transaction_components::{ + KernelBuilder, + RangeProofType, + Transaction, + TransactionKernel, + TransactionKernelVersion, + }, + }, }; +use tari_key_manager::key_manager_service::KeyManagerInterface; use tari_p2p::{auto_update::SoftwareUpdaterHandle, services::liveness::LivenessHandle}; use tari_utilities::{hex::Hex, message_format::MessageFormat, ByteArray}; use tokio::task; @@ -123,6 +143,8 @@ impl BaseNodeGrpcServer { let mining_method = vec![ GrpcMethod::GetVersion, GrpcMethod::GetNewBlockTemplate, + GrpcMethod::GetNewBlockWithCoinbases, + GrpcMethod::GetNewBlockTemplateWithCoinbases, GrpcMethod::GetNewBlock, GrpcMethod::GetNewBlockBlob, GrpcMethod::SubmitBlock, @@ -147,7 +169,6 @@ impl BaseNodeGrpcServer { if self.config.second_layer_grpc_enabled && second_layer_methods.contains(&grpc_method) { return true; } - self.config.grpc_server_allow_methods.contains(&grpc_method) } } @@ -625,9 +646,446 @@ impl tari_rpc::base_node_server::BaseNode for BaseNodeGrpcServer { Status::invalid_argument(format!("Malformed block template provided: {}", s)), ) })?; + let algo = block_template.header.pow.pow_algo; + + let mut handler = self.node_service.clone(); + + let new_block = match handler.get_new_block(block_template).await { + Ok(b) => b, + Err(CommsInterfaceError::ChainStorageError(ChainStorageError::InvalidArguments { message, .. })) => { + return Err(obscure_error_if_true( + report_error_flag, + Status::invalid_argument(message), + )); + }, + Err(CommsInterfaceError::ChainStorageError(ChainStorageError::CannotCalculateNonTipMmr(msg))) => { + let status = Status::with_details( + tonic::Code::FailedPrecondition, + msg, + Bytes::from_static(b"CannotCalculateNonTipMmr"), + ); + return Err(obscure_error_if_true(report_error_flag, status)); + }, + Err(e) => { + return Err(obscure_error_if_true( + report_error_flag, + Status::internal(e.to_string()), + )) + }, + }; + let fees = new_block.body.get_total_fee().map_err(|_| { + obscure_error_if_true( + report_error_flag, + Status::invalid_argument("Invalid fees in block".to_string()), + ) + })?; + let gen_hash = handler + .get_header(0) + .await + .map_err(|_| { + obscure_error_if_true( + report_error_flag, + Status::invalid_argument("Tari genesis block not found".to_string()), + ) + })? + .ok_or_else(|| { + obscure_error_if_true( + report_error_flag, + Status::not_found("Tari genesis block not found".to_string()), + ) + })? + .hash() + .to_vec(); + // construct response + let block_hash = new_block.hash().to_vec(); + let mining_hash = match new_block.header.pow.pow_algo { + PowAlgorithm::Sha3x => new_block.header.mining_hash().to_vec(), + PowAlgorithm::RandomX => new_block.header.merge_mining_hash().to_vec(), + }; + let block: Option = Some( + new_block + .try_into() + .map_err(|e| obscure_error_if_true(report_error_flag, Status::internal(e)))?, + ); + let new_template = handler.get_new_block_template(algo, 0).await.map_err(|e| { + warn!( + target: LOG_TARGET, + "Could not get new block template: {}", + e.to_string() + ); + obscure_error_if_true(report_error_flag, Status::internal(e.to_string())) + })?; + + let pow = algo as i32; + + let miner_data = tari_rpc::MinerData { + reward: new_template.reward.into(), + target_difficulty: new_template.target_difficulty.as_u64(), + total_fees: fees.as_u64(), + algo: Some(tari_rpc::PowAlgo { pow_algo: pow }), + }; + + let response = tari_rpc::GetNewBlockResult { + block_hash, + block, + merge_mining_hash: mining_hash, + tari_unique_id: gen_hash, + miner_data: Some(miner_data), + }; + debug!(target: LOG_TARGET, "Sending GetNewBlock response to client"); + Ok(Response::new(response)) + } + + #[allow(clippy::too_many_lines)] + async fn get_new_block_template_with_coinbases( + &self, + request: Request, + ) -> Result, Status> { + if !self.is_method_enabled(GrpcMethod::GetNewBlockTemplateWithCoinbases) { + return Err(Status::permission_denied( + "`GetNewBlockTemplateWithCoinbases` method not made available", + )); + } + debug!(target: LOG_TARGET, "Incoming GRPC request for get new block template with coinbases"); + let report_error_flag = self.report_error_flag(); + let request = request.into_inner(); + let algo = request + .algo + .map(|algo| u64::try_from(algo.pow_algo)) + .ok_or_else(|| obscure_error_if_true(report_error_flag, Status::invalid_argument("PoW algo not provided")))? + .map_err(|e| { + obscure_error_if_true( + report_error_flag, + Status::invalid_argument(format!("Invalid PoW algo '{}'", e)), + ) + })?; + + let algo = PowAlgorithm::try_from(algo).map_err(|e| { + obscure_error_if_true( + report_error_flag, + Status::invalid_argument(format!("Invalid PoW algo '{}'", e)), + ) + })?; + + let mut handler = self.node_service.clone(); + + let mut new_template = handler + .get_new_block_template(algo, request.max_weight) + .await + .map_err(|e| { + warn!( + target: LOG_TARGET, + "Could not get new block template: {}", + e.to_string() + ); + obscure_error_if_true(report_error_flag, Status::internal(e.to_string())) + })?; + + let pow = algo as i32; + + let miner_data = tari_rpc::MinerData { + reward: new_template.reward.into(), + target_difficulty: new_template.target_difficulty.as_u64(), + total_fees: new_template.total_fees.into(), + algo: Some(tari_rpc::PowAlgo { pow_algo: pow }), + }; + + let mut coinbases: Vec = request.coinbases; + + // let validate the coinbase amounts; + let reward = self + .consensus_rules + .calculate_coinbase_and_fees(new_template.header.height, new_template.body.kernels()) + .map_err(|_| { + obscure_error_if_true( + report_error_flag, + Status::internal("Could not calculate the amount of fees in the block".to_string()), + ) + })? + .as_u64(); + let mut total_shares = 0u64; + for coinbase in &coinbases { + total_shares += coinbase.value; + } + let mut remainder = reward - ((reward / total_shares) * total_shares); + for coinbase in &mut coinbases { + coinbase.value *= reward / total_shares; + if remainder > 0 { + coinbase.value += 1; + remainder -= 1; + } + } + + let key_manager = create_memory_db_key_manager(); + let height = new_template.header.height; + // The script key is not used in the Diffie-Hellmann protocol, so we assign default. + let script_key_id = TariKeyId::default(); + + let mut total_excess = Commitment::default(); + let mut total_nonce = PublicKey::default(); + let mut private_keys = Vec::new(); + let mut kernel_message = [0; 32]; + let mut last_kernel = Default::default(); + for coinbase in coinbases { + let address = TariAddress::from_hex(&coinbase.address) + .map_err(|e| obscure_error_if_true(report_error_flag, Status::internal(e.to_string())))?; + let range_proof_type = if coinbase.revealed_value_proof { + RangeProofType::RevealedValue + } else { + RangeProofType::BulletProofPlus + }; + let (_, coinbase_output, coinbase_kernel, wallet_output) = generate_coinbase_with_wallet_output( + 0.into(), + coinbase.value.into(), + height, + &coinbase.coinbase_extra, + &key_manager, + &script_key_id, + &address, + coinbase.stealth_payment, + self.consensus_rules.consensus_constants(height), + range_proof_type, + ) + .await + .map_err(|e| obscure_error_if_true(report_error_flag, Status::internal(e.to_string())))?; + new_template.body.add_output(coinbase_output); + let (new_private_nonce, pub_nonce) = key_manager + .get_next_key(TransactionKeyManagerBranch::KernelNonce.get_branch_key()) + .await + .map_err(|e| obscure_error_if_true(report_error_flag, Status::internal(e.to_string())))?; + total_nonce = &total_nonce + &pub_nonce; + total_excess = &total_excess + &coinbase_kernel.excess; + private_keys.push((wallet_output.spending_key_id, new_private_nonce)); + kernel_message = TransactionKernel::build_kernel_signature_message( + &TransactionKernelVersion::get_current_version(), + coinbase_kernel.fee, + coinbase_kernel.lock_height, + &coinbase_kernel.features, + &None, + ); + last_kernel = coinbase_kernel; + } + let mut kernel_signature = Signature::default(); + for (spending_key_id, nonce) in private_keys { + kernel_signature = &kernel_signature + + &key_manager + .get_partial_txo_kernel_signature( + &spending_key_id, + &nonce, + &total_nonce, + total_excess.as_public_key(), + &TransactionKernelVersion::get_current_version(), + &kernel_message, + &last_kernel.features, + TxoStage::Output, + ) + .await + .map_err(|e| obscure_error_if_true(report_error_flag, Status::internal(e.to_string())))?; + } + let kernel_new = KernelBuilder::new() + .with_fee(0.into()) + .with_features(last_kernel.features) + .with_lock_height(last_kernel.lock_height) + .with_excess(&total_excess) + .with_signature(kernel_signature) + .build() + .unwrap(); + + new_template.body.add_kernel(kernel_new); + new_template.body.sort(); + + let new_block = match handler.get_new_block(new_template).await { + Ok(b) => b, + Err(CommsInterfaceError::ChainStorageError(ChainStorageError::InvalidArguments { message, .. })) => { + return Err(obscure_error_if_true( + report_error_flag, + Status::invalid_argument(message), + )); + }, + Err(CommsInterfaceError::ChainStorageError(ChainStorageError::CannotCalculateNonTipMmr(msg))) => { + let status = Status::with_details( + tonic::Code::FailedPrecondition, + msg, + Bytes::from_static(b"CannotCalculateNonTipMmr"), + ); + return Err(obscure_error_if_true(report_error_flag, status)); + }, + Err(e) => { + return Err(obscure_error_if_true( + report_error_flag, + Status::internal(e.to_string()), + )) + }, + }; + let gen_hash = handler + .get_header(0) + .await + .map_err(|_| { + obscure_error_if_true( + report_error_flag, + Status::invalid_argument("Tari genesis block not found".to_string()), + ) + })? + .ok_or_else(|| { + obscure_error_if_true( + report_error_flag, + Status::not_found("Tari genesis block not found".to_string()), + ) + })? + .hash() + .to_vec(); + // construct response + let block_hash = new_block.hash().to_vec(); + let mining_hash = match new_block.header.pow.pow_algo { + PowAlgorithm::Sha3x => new_block.header.mining_hash().to_vec(), + PowAlgorithm::RandomX => new_block.header.merge_mining_hash().to_vec(), + }; + let block: Option = Some( + new_block + .try_into() + .map_err(|e| obscure_error_if_true(report_error_flag, Status::internal(e)))?, + ); + + let response = tari_rpc::GetNewBlockResult { + block_hash, + block, + merge_mining_hash: mining_hash, + tari_unique_id: gen_hash, + miner_data: Some(miner_data), + }; + debug!(target: LOG_TARGET, "Sending GetNewBlock response to client"); + Ok(Response::new(response)) + } + + #[allow(clippy::too_many_lines)] + async fn get_new_block_with_coinbases( + &self, + request: Request, + ) -> Result, Status> { + if !self.is_method_enabled(GrpcMethod::GetNewBlockWithCoinbases) { + return Err(Status::permission_denied( + "`GetNewBlockWithCoinbasesRequest` method not made available", + )); + } + let report_error_flag = self.report_error_flag(); + let request = request.into_inner(); + debug!(target: LOG_TARGET, "Incoming GRPC request for get new block with coinbases"); + let mut block_template: NewBlockTemplate = request + .new_template + .ok_or(obscure_error_if_true( + report_error_flag, + Status::invalid_argument("Malformed block template provided".to_string()), + ))? + .try_into() + .map_err(|s| { + obscure_error_if_true( + report_error_flag, + Status::invalid_argument(format!("Malformed block template provided: {}", s)), + ) + })?; + let coinbases: Vec = request.coinbases; let mut handler = self.node_service.clone(); + // let validate the coinbase amounts; + let reward = self + .consensus_rules + .calculate_coinbase_and_fees(block_template.header.height, block_template.body.kernels()) + .map_err(|_| { + obscure_error_if_true( + report_error_flag, + Status::internal("Could not calculate the amount of fees in the block".to_string()), + ) + })?; + let mut amount = 0u64; + for coinbase in &coinbases { + amount += coinbase.value; + } + + if amount != reward.as_u64() { + return Err(obscure_error_if_true( + report_error_flag, + Status::invalid_argument("Malformed coinbase amounts".to_string()), + )); + } + let key_manager = create_memory_db_key_manager(); + let height = block_template.header.height; + // The script key is not used in the Diffie-Hellmann protocol, so we assign default. + let script_key_id = TariKeyId::default(); + + let mut total_excess = Commitment::default(); + let mut total_nonce = PublicKey::default(); + let mut private_keys = Vec::new(); + let mut kernel_message = [0; 32]; + let mut last_kernel = Default::default(); + for coinbase in coinbases { + let address = TariAddress::from_hex(&coinbase.address) + .map_err(|e| obscure_error_if_true(report_error_flag, Status::internal(e.to_string())))?; + let range_proof_type = if coinbase.revealed_value_proof { + RangeProofType::RevealedValue + } else { + RangeProofType::BulletProofPlus + }; + let (_, coinbase_output, coinbase_kernel, wallet_output) = generate_coinbase_with_wallet_output( + 0.into(), + coinbase.value.into(), + height, + &coinbase.coinbase_extra, + &key_manager, + &script_key_id, + &address, + coinbase.stealth_payment, + self.consensus_rules.consensus_constants(height), + range_proof_type, + ) + .await + .map_err(|e| obscure_error_if_true(report_error_flag, Status::internal(e.to_string())))?; + block_template.body.add_output(coinbase_output); + let (new_private_nonce, pub_nonce) = key_manager + .get_next_key(TransactionKeyManagerBranch::KernelNonce.get_branch_key()) + .await + .map_err(|e| obscure_error_if_true(report_error_flag, Status::internal(e.to_string())))?; + total_nonce = &total_nonce + &pub_nonce; + total_excess = &total_excess + &coinbase_kernel.excess; + private_keys.push((wallet_output.spending_key_id, new_private_nonce)); + kernel_message = TransactionKernel::build_kernel_signature_message( + &TransactionKernelVersion::get_current_version(), + coinbase_kernel.fee, + coinbase_kernel.lock_height, + &coinbase_kernel.features, + &None, + ); + last_kernel = coinbase_kernel; + } + let mut kernel_signature = Signature::default(); + for (spending_key_id, nonce) in private_keys { + kernel_signature = &kernel_signature + + &key_manager + .get_partial_txo_kernel_signature( + &spending_key_id, + &nonce, + &total_nonce, + total_excess.as_public_key(), + &TransactionKernelVersion::get_current_version(), + &kernel_message, + &last_kernel.features, + TxoStage::Output, + ) + .await + .map_err(|e| obscure_error_if_true(report_error_flag, Status::internal(e.to_string())))?; + } + let kernel_new = KernelBuilder::new() + .with_fee(0.into()) + .with_features(last_kernel.features) + .with_lock_height(last_kernel.lock_height) + .with_excess(&total_excess) + .with_signature(kernel_signature) + .build() + .unwrap(); + + block_template.body.add_kernel(kernel_new); + block_template.body.sort(); + let new_block = match handler.get_new_block(block_template).await { Ok(b) => b, Err(CommsInterfaceError::ChainStorageError(ChainStorageError::InvalidArguments { message, .. })) => { @@ -651,6 +1109,13 @@ impl tari_rpc::base_node_server::BaseNode for BaseNodeGrpcServer { )) }, }; + let fees = new_block.body.get_total_fee().map_err(|_| { + obscure_error_if_true( + report_error_flag, + Status::invalid_argument("Invalid fees in block".to_string()), + ) + })?; + let algo = new_block.header.pow.pow_algo; let gen_hash = handler .get_header(0) .await @@ -680,11 +1145,30 @@ impl tari_rpc::base_node_server::BaseNode for BaseNodeGrpcServer { .map_err(|e| obscure_error_if_true(report_error_flag, Status::internal(e)))?, ); + let new_template = handler.get_new_block_template(algo, 0).await.map_err(|e| { + warn!( + target: LOG_TARGET, + "Could not get new block template: {}", + e.to_string() + ); + obscure_error_if_true(report_error_flag, Status::internal(e.to_string())) + })?; + + let pow = algo as i32; + + let miner_data = tari_rpc::MinerData { + reward: new_template.reward.into(), + target_difficulty: new_template.target_difficulty.as_u64(), + total_fees: fees.as_u64(), + algo: Some(tari_rpc::PowAlgo { pow_algo: pow }), + }; + let response = tari_rpc::GetNewBlockResult { block_hash, block, merge_mining_hash: mining_hash, tari_unique_id: gen_hash, + miner_data: Some(miner_data), }; debug!(target: LOG_TARGET, "Sending GetNewBlock response to client"); Ok(Response::new(response)) diff --git a/base_layer/core/src/transactions/coinbase_builder.rs b/base_layer/core/src/transactions/coinbase_builder.rs index cb2f27087f..ead4800baa 100644 --- a/base_layer/core/src/transactions/coinbase_builder.rs +++ b/base_layer/core/src/transactions/coinbase_builder.rs @@ -729,7 +729,7 @@ mod test { TransactionKeyManagerInterface, TxoStage, }, - transaction_components::{RangeProofType, TransactionKernelVersion}, + transaction_components::{KernelBuilder, RangeProofType, TransactionKernelVersion}, }; #[tokio::test] @@ -863,4 +863,134 @@ mod test { ) .unwrap(); } + + #[tokio::test] + #[allow(clippy::too_many_lines)] + #[allow(clippy::identity_op)] + async fn multi_coinbase_amount() { + // We construct two txs both valid with a single coinbase. We then add a duplicate coinbase utxo to the one, and + // a duplicate coinbase kernel to the other one. + let (builder, rules, factories, key_manager) = get_builder(); + let p = TestParams::new(&key_manager).await; + // We just want some small amount here. + let missing_fee = rules.emission_schedule().block_reward(4200000) + (2 * uT); + let wallet_payment_address = TariAddress::default(); + let builder = builder + .with_block_height(42) + .with_fees(1 * uT) + .with_spend_key_id(p.spend_key_id.clone()) + .with_encryption_key_id(TariKeyId::default()) + .with_sender_offset_key_id(p.sender_offset_key_id.clone()) + .with_script_key_id(p.script_key_id.clone()) + .with_script(one_sided_payment_script(wallet_payment_address.public_key())) + .with_range_proof_type(RangeProofType::RevealedValue); + let (tx1, wo1) = builder + .build(rules.consensus_constants(0), rules.emission_schedule()) + .await + .unwrap(); + + // we calculate a duplicate tx here so that we can have a coinbase with the correct fee amount + let block_reward = rules.emission_schedule().block_reward(42) + missing_fee; + let builder = CoinbaseBuilder::new(key_manager.clone()); + let builder = builder + .with_block_height(4200000) + .with_fees(1 * uT) + .with_spend_key_id(p.spend_key_id.clone()) + .with_encryption_key_id(TariKeyId::default()) + .with_sender_offset_key_id(p.sender_offset_key_id) + .with_script_key_id(p.script_key_id) + .with_script(one_sided_payment_script(wallet_payment_address.public_key())) + .with_range_proof_type(RangeProofType::RevealedValue); + let (tx2, wo2) = builder + .build(rules.consensus_constants(0), rules.emission_schedule()) + .await + .unwrap(); + + let coinbase1 = tx1.body.outputs()[0].clone(); + let coinbase2 = tx2.body.outputs()[0].clone(); + let mut kernel_1 = tx1.body.kernels()[0].clone(); + let kernel_2 = tx2.body.kernels()[0].clone(); + let excess = &kernel_1.excess + &kernel_2.excess; + kernel_1.excess = &kernel_1.excess + &kernel_2.excess; + kernel_1.excess_sig = &kernel_1.excess_sig + &kernel_2.excess_sig; + let mut body1 = AggregateBody::new(Vec::new(), vec![coinbase1, coinbase2], vec![kernel_1.clone()]); + body1.sort(); + + body1 + .check_coinbase_output( + block_reward, + rules.consensus_constants(0).coinbase_min_maturity(), + &factories, + 42, + ) + .unwrap(); + body1.verify_kernel_signatures().unwrap_err(); + + // lets create a new kernel with a correct signature + let (new_nonce1, nonce1) = key_manager + .get_next_key(TransactionKeyManagerBranch::KernelNonce.get_branch_key()) + .await + .unwrap(); + let (new_nonce2, nonce2) = key_manager + .get_next_key(TransactionKeyManagerBranch::KernelNonce.get_branch_key()) + .await + .unwrap(); + let nonce = &nonce1 + &nonce2; + let kernel_message = TransactionKernel::build_kernel_signature_message( + &TransactionKernelVersion::get_current_version(), + kernel_1.fee, + kernel_1.lock_height, + &kernel_1.features, + &None, + ); + + let mut kernel_signature = key_manager + .get_partial_txo_kernel_signature( + &wo1.spending_key_id, + &new_nonce1, + &nonce, + excess.as_public_key(), + &TransactionKernelVersion::get_current_version(), + &kernel_message, + &kernel_1.features, + TxoStage::Output, + ) + .await + .unwrap(); + kernel_signature = &kernel_signature + + &key_manager + .get_partial_txo_kernel_signature( + &wo2.spending_key_id, + &new_nonce2, + &nonce, + excess.as_public_key(), + &TransactionKernelVersion::get_current_version(), + &kernel_message, + &kernel_1.features, + TxoStage::Output, + ) + .await + .unwrap(); + let kernel_new = KernelBuilder::new() + .with_fee(0.into()) + .with_features(kernel_1.features) + .with_lock_height(kernel_1.lock_height) + .with_excess(&excess) + .with_signature(kernel_signature) + .build() + .unwrap(); + + let mut body2 = AggregateBody::new(Vec::new(), body1.outputs().clone(), vec![kernel_new]); + body2.sort(); + + body2 + .check_coinbase_output( + block_reward, + rules.consensus_constants(0).coinbase_min_maturity(), + &factories, + 42, + ) + .unwrap(); + body2.verify_kernel_signatures().unwrap(); + } } diff --git a/base_layer/core/src/transactions/transaction_components/transaction_kernel.rs b/base_layer/core/src/transactions/transaction_components/transaction_kernel.rs index 3f482f0c05..85d60c166f 100644 --- a/base_layer/core/src/transactions/transaction_components/transaction_kernel.rs +++ b/base_layer/core/src/transactions/transaction_components/transaction_kernel.rs @@ -51,7 +51,7 @@ use crate::{ /// [Mimblewimble TLU post](https://tlu.tarilabs.com/protocols/mimblewimble-1/sources/PITCHME.link.html?highlight=mimblewimble#mimblewimble). /// The kernel also tracks other transaction metadata, such as the lock height for the transaction (i.e. the earliest /// this transaction can be mined) and the transaction fee, in cleartext. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, Default)] pub struct TransactionKernel { pub version: TransactionKernelVersion, /// Options for a kernel's structure or use diff --git a/common/config/presets/c_base_node_b_mining_allow_methods.toml b/common/config/presets/c_base_node_b_mining_allow_methods.toml index 8a2d6113dc..085017b383 100644 --- a/common/config/presets/c_base_node_b_mining_allow_methods.toml +++ b/common/config/presets/c_base_node_b_mining_allow_methods.toml @@ -33,6 +33,8 @@ grpc_server_allow_methods = [ "get_network_difficulty", "get_new_block_template", "get_new_block", + "get_new_block_with_coinbases", + "get_new_block_template_with_coinbases", "get_new_block_blob", "submit_block", "submit_block_blob", diff --git a/common/config/presets/c_base_node_b_non_mining_allow_methods.toml b/common/config/presets/c_base_node_b_non_mining_allow_methods.toml index 92ebf7cf34..bf27e6186c 100644 --- a/common/config/presets/c_base_node_b_non_mining_allow_methods.toml +++ b/common/config/presets/c_base_node_b_non_mining_allow_methods.toml @@ -32,6 +32,8 @@ grpc_server_allow_methods = [ #"get_tokens_in_circulation", #"get_network_difficulty", #"get_new_block_template", + #"get_new_block_with_coinbases", + #"get_new_block_template_with_coinbases", #"get_new_block", #"get_new_block_blob", #"submit_block", diff --git a/integration_tests/src/base_node_process.rs b/integration_tests/src/base_node_process.rs index d24e16fb29..675bee11ff 100644 --- a/integration_tests/src/base_node_process.rs +++ b/integration_tests/src/base_node_process.rs @@ -204,6 +204,8 @@ pub async fn spawn_base_node_with_config( GrpcMethod::GetNetworkDifficulty, GrpcMethod::GetNewBlockTemplate, GrpcMethod::GetNewBlock, + GrpcMethod::GetNewBlockWithCoinbases, + GrpcMethod::GetNewBlockTemplateWithCoinbases, GrpcMethod::GetNewBlockBlob, GrpcMethod::SubmitBlock, GrpcMethod::SubmitBlockBlob, diff --git a/integration_tests/tests/features/BlockTemplate.feature b/integration_tests/tests/features/BlockTemplate.feature index c7854f2dd1..12b4533c7f 100644 --- a/integration_tests/tests/features/BlockTemplate.feature +++ b/integration_tests/tests/features/BlockTemplate.feature @@ -9,3 +9,10 @@ Scenario: Verify UTXO and kernel MMR size in header Given I have a seed node SEED_A When I have 1 base nodes connected to all seed nodes Then meddling with block template data from node SEED_A is not allowed + + @critical + Scenario: Verify gprc cna create block with more than 1 coinbase + Given I have a seed node SEED_A + When I have 1 base nodes connected to all seed nodes + Then generate a block with 2 coinbases from node SEED_A + Then generate a block with 2 coinbases as a single request from node SEED_A \ No newline at end of file diff --git a/integration_tests/tests/steps/node_steps.rs b/integration_tests/tests/steps/node_steps.rs index 7b3e325359..f6c1049ab6 100644 --- a/integration_tests/tests/steps/node_steps.rs +++ b/integration_tests/tests/steps/node_steps.rs @@ -20,15 +20,29 @@ // 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 std::{convert::TryFrom, time::Duration}; +use std::{ + convert::{TryFrom, TryInto}, + time::Duration, +}; use cucumber::{given, then, when}; use futures::StreamExt; use indexmap::IndexMap; -use minotari_app_grpc::tari_rpc::{self as grpc, GetBlocksRequest, ListHeadersRequest}; +use minotari_app_grpc::tari_rpc::{ + self as grpc, + pow_algo::PowAlgos, + GetBlocksRequest, + GetNewBlockTemplateWithCoinbasesRequest, + GetNewBlockWithCoinbasesRequest, + ListHeadersRequest, + NewBlockCoinbase, + NewBlockTemplateRequest, + PowAlgo, +}; use minotari_node::BaseNodeConfig; use minotari_wallet_grpc_client::grpc::{Empty, GetIdentityRequest}; -use tari_core::blocks::Block; +use tari_common_types::tari_address::TariAddress; +use tari_core::{blocks::Block, transactions::aggregated_body::AggregateBody}; use tari_integration_tests::{ base_node_process::{spawn_base_node, spawn_base_node_with_config}, get_peer_addresses, @@ -719,6 +733,143 @@ async fn no_meddling_with_data(world: &mut TariWorld, node: String) { } } +#[then(expr = "generate a block with 2 coinbases from node {word}")] +async fn generate_block_with_2_coinbases(world: &mut TariWorld, node: String) { + let mut client = world.get_node_client(&node).await.unwrap(); + + let template_req = NewBlockTemplateRequest { + algo: Some(PowAlgo { + pow_algo: PowAlgos::Sha3x.into(), + }), + max_weight: 0, + }; + + let template_response = client.get_new_block_template(template_req).await.unwrap().into_inner(); + + let block_template = template_response.new_block_template.clone().unwrap(); + let miner_data = template_response.miner_data.clone().unwrap(); + let amount = miner_data.reward + miner_data.total_fees; + let request = GetNewBlockWithCoinbasesRequest { + new_template: Some(block_template), + coinbases: vec![ + NewBlockCoinbase { + address: TariAddress::from_hex("30a815df7b8d7f653ce3252f08a21d570b1ac44958cb4d7af0e0ef124f89b11943") + .unwrap() + .to_hex(), + value: amount - 1000, + stealth_payment: false, + revealed_value_proof: true, + coinbase_extra: Vec::new(), + }, + NewBlockCoinbase { + address: TariAddress::from_hex("3e596f98f6904f0fc1c8685e2274bd8b2c445d5dac284a9398d09a0e9a760436d0") + .unwrap() + .to_hex(), + value: 1000, + stealth_payment: false, + revealed_value_proof: true, + coinbase_extra: Vec::new(), + }, + ], + }; + + let new_block = client.get_new_block_with_coinbases(request).await.unwrap().into_inner(); + + let new_block = new_block.block.unwrap(); + let mut coinbase_kernel_count = 0; + let mut coinbase_utxo_count = 0; + let body: AggregateBody = new_block.body.clone().unwrap().try_into().unwrap(); + for kernel in body.kernels() { + if kernel.is_coinbase() { + coinbase_kernel_count += 1; + } + } + for utxo in body.outputs() { + if utxo.is_coinbase() { + coinbase_utxo_count += 1; + } + } + assert_eq!(coinbase_kernel_count, 1); + assert_eq!(coinbase_utxo_count, 2); + + match client.submit_block(new_block).await { + Ok(_) => (), + Err(e) => panic!("The block should have been valid, {}", e), + } +} + +#[then(expr = "generate a block with 2 coinbases as a single request from node {word}")] +async fn generate_block_with_2_as_single_request_coinbases(world: &mut TariWorld, node: String) { + let mut client = world.get_node_client(&node).await.unwrap(); + + let template_req = GetNewBlockTemplateWithCoinbasesRequest { + algo: Some(PowAlgo { + pow_algo: PowAlgos::Sha3x.into(), + }), + max_weight: 0, + coinbases: vec![ + NewBlockCoinbase { + address: TariAddress::from_hex("30a815df7b8d7f653ce3252f08a21d570b1ac44958cb4d7af0e0ef124f89b11943") + .unwrap() + .to_hex(), + value: 1, + stealth_payment: false, + revealed_value_proof: true, + coinbase_extra: Vec::new(), + }, + NewBlockCoinbase { + address: TariAddress::from_hex("3e596f98f6904f0fc1c8685e2274bd8b2c445d5dac284a9398d09a0e9a760436d0") + .unwrap() + .to_hex(), + value: 2, + stealth_payment: false, + revealed_value_proof: true, + coinbase_extra: Vec::new(), + }, + ], + }; + let new_block = client + .get_new_block_template_with_coinbases(template_req) + .await + .unwrap() + .into_inner(); + + let new_block = new_block.block.unwrap(); + let mut coinbase_kernel_count = 0; + let mut coinbase_utxo_count = 0; + let body: AggregateBody = new_block.body.clone().unwrap().try_into().unwrap(); + for kernel in body.kernels() { + if kernel.is_coinbase() { + coinbase_kernel_count += 1; + } + } + println!("{}", body); + for utxo in body.outputs() { + if utxo.is_coinbase() { + coinbase_utxo_count += 1; + } + } + assert_eq!(coinbase_kernel_count, 1); + assert_eq!(coinbase_utxo_count, 2); + let mut num_6154266700 = 0; + let mut num_12308533398 = 0; + for output in body.outputs() { + if output.minimum_value_promise.as_u64() == 6154266700 { + num_6154266700 += 1; + } + if output.minimum_value_promise.as_u64() == 12308533398 { + num_12308533398 += 1; + } + } + assert_eq!(num_6154266700, 1); + assert_eq!(num_12308533398, 1); + + match client.submit_block(new_block).await { + Ok(_) => (), + Err(e) => panic!("The block should have been valid, {}", e), + } +} + #[when(expr = "I have a lagging delayed node {word} connected to node {word} with \ blocks_behind_before_considered_lagging {int}")] async fn lagging_delayed_node(world: &mut TariWorld, delayed_node: String, node: String, delay: u64) {