diff --git a/consensus/core/src/tx.rs b/consensus/core/src/tx.rs index 4d4ce8b7a..7a1c1e35d 100644 --- a/consensus/core/src/tx.rs +++ b/consensus/core/src/tx.rs @@ -25,7 +25,7 @@ pub struct ScriptPublicKey { impl ScriptPublicKey { pub fn new(version: u16, script: ScriptVec) -> Self { - Self { script, version } + Self { version, script } } pub fn from_vec(version: u16, script: Vec) -> Self { @@ -81,7 +81,7 @@ impl Display for TransactionOutpoint { } /// Represents a Kaspa transaction input -#[derive(Debug, Serialize, Deserialize, Clone)] // TODO: Implement a custom serializer for input that drops utxo_entry +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct TransactionInput { pub previous_outpoint: TransactionOutpoint, pub signature_script: Vec, diff --git a/consensus/src/consensus/mod.rs b/consensus/src/consensus/mod.rs index 9fa56f672..cf0a90cf8 100644 --- a/consensus/src/consensus/mod.rs +++ b/consensus/src/consensus/mod.rs @@ -225,7 +225,7 @@ impl Consensus { reachability_store.clone(), ghostdag_store.clone(), headers_store.clone(), - daa_store, + daa_store.clone(), statuses_store.clone(), pruning_store.clone(), depth_store, @@ -270,6 +270,7 @@ impl Consensus { statuses_store.clone(), ghostdag_store.clone(), headers_store.clone(), + daa_store, block_transactions_store, pruning_store.clone(), past_pruning_points_store, @@ -281,6 +282,7 @@ impl Consensus { reachability_service.clone(), dag_traversal_manager.clone(), difficulty_manager.clone(), + coinbase_manager.clone(), transaction_validator, pruning_manager.clone(), )); diff --git a/consensus/src/consensus/test_consensus.rs b/consensus/src/consensus/test_consensus.rs index 33708554b..fd772450a 100644 --- a/consensus/src/consensus/test_consensus.rs +++ b/consensus/src/consensus/test_consensus.rs @@ -65,7 +65,7 @@ impl TestConsensus { let (daa_score, _) = self .consensus .difficulty_manager - .calc_daa_score_and_added_blocks(&mut window.iter().map(|item| item.0.hash), &ghostdag_data); + .calc_daa_score_and_non_daa_mergeset_blocks(&mut window.iter().map(|item| item.0.hash), &ghostdag_data); header.bits = self.consensus.difficulty_manager.calculate_difficulty_bits(&window); header.daa_score = daa_score; header.timestamp = self.consensus.past_median_time_manager.calc_past_median_time(ghostdag_data.clone()).0 + 1; diff --git a/consensus/src/model/stores/daa.rs b/consensus/src/model/stores/daa.rs index d9dbe9db9..b642d17f9 100644 --- a/consensus/src/model/stores/daa.rs +++ b/consensus/src/model/stores/daa.rs @@ -5,26 +5,26 @@ use super::{ errors::StoreError, DB, }; -use consensus_core::{blockhash::BlockHashes, BlockHasher}; +use consensus_core::{BlockHashSet, BlockHasher}; use hashes::Hash; use rocksdb::WriteBatch; pub trait DaaStoreReader { - fn get_daa_added_blocks(&self, hash: Hash) -> Result; + fn get_mergeset_non_daa(&self, hash: Hash) -> Result, StoreError>; } pub trait DaaStore: DaaStoreReader { // This is append only - fn insert(&self, hash: Hash, added_blocks: BlockHashes) -> Result<(), StoreError>; + fn insert(&self, hash: Hash, mergeset_non_daa: Arc) -> Result<(), StoreError>; } -const STORE_PREFIX: &[u8] = b"daa-added-blocks"; +const STORE_PREFIX: &[u8] = b"mergeset_non_daa"; /// A DB + cache implementation of `DaaStore` trait, with concurrency support. #[derive(Clone)] pub struct DbDaaStore { db: Arc, - access: CachedDbAccess, + access: CachedDbAccess, BlockHasher>, } impl DbDaaStore { @@ -36,27 +36,27 @@ impl DbDaaStore { Self::new(Arc::clone(&self.db), cache_size) } - pub fn insert_batch(&self, batch: &mut WriteBatch, hash: Hash, added_blocks: BlockHashes) -> Result<(), StoreError> { + pub fn insert_batch(&self, batch: &mut WriteBatch, hash: Hash, mergeset_non_daa: Arc) -> Result<(), StoreError> { if self.access.has(hash)? { return Err(StoreError::KeyAlreadyExists(hash.to_string())); } - self.access.write(BatchDbWriter::new(batch), hash, added_blocks)?; + self.access.write(BatchDbWriter::new(batch), hash, mergeset_non_daa)?; Ok(()) } } impl DaaStoreReader for DbDaaStore { - fn get_daa_added_blocks(&self, hash: Hash) -> Result { + fn get_mergeset_non_daa(&self, hash: Hash) -> Result, StoreError> { self.access.read(hash) } } impl DaaStore for DbDaaStore { - fn insert(&self, hash: Hash, added_blocks: BlockHashes) -> Result<(), StoreError> { + fn insert(&self, hash: Hash, mergeset_non_daa: Arc) -> Result<(), StoreError> { if self.access.has(hash)? { return Err(StoreError::KeyAlreadyExists(hash.to_string())); } - self.access.write(DirectDbWriter::new(&self.db), hash, added_blocks)?; + self.access.write(DirectDbWriter::new(&self.db), hash, mergeset_non_daa)?; Ok(()) } } diff --git a/consensus/src/model/stores/database/key.rs b/consensus/src/model/stores/database/key.rs index 268f31f3f..448935fe8 100644 --- a/consensus/src/model/stores/database/key.rs +++ b/consensus/src/model/stores/database/key.rs @@ -17,7 +17,7 @@ impl DbKey { } pub fn prefix_only(prefix: &[u8]) -> Self { - Self::new(prefix, &[]) + Self::new(prefix, []) } } diff --git a/consensus/src/pipeline/body_processor/body_validation_in_context.rs b/consensus/src/pipeline/body_processor/body_validation_in_context.rs index eebf084d5..6484cc6ef 100644 --- a/consensus/src/pipeline/body_processor/body_validation_in_context.rs +++ b/consensus/src/pipeline/body_processor/body_validation_in_context.rs @@ -58,7 +58,7 @@ impl BlockBodyProcessor { } fn check_coinbase_blue_score_and_subsidy(self: &Arc, block: &Block) -> BlockProcessResult<()> { - match self.coinbase_manager.validate_coinbase_payload_in_isolation_and_extract_coinbase_data(&block.transactions[0]) { + match self.coinbase_manager.deserialize_coinbase_payload(&block.transactions[0].payload) { Ok(data) => { if data.blue_score != block.header.blue_score { return Err(RuleError::BadCoinbasePayloadBlueScore(data.blue_score, block.header.blue_score)); diff --git a/consensus/src/pipeline/header_processor/pre_pow_validation.rs b/consensus/src/pipeline/header_processor/pre_pow_validation.rs index 6c9608d31..be07e1573 100644 --- a/consensus/src/pipeline/header_processor/pre_pow_validation.rs +++ b/consensus/src/pipeline/header_processor/pre_pow_validation.rs @@ -58,17 +58,17 @@ impl HeaderProcessor { header: &Header, ) -> BlockProcessResult<()> { let ghostdag_data = ctx.ghostdag_data.clone().unwrap(); - let window = self.dag_traversal_manager.block_window(ghostdag_data, self.difficulty_window_size); + let window = self.dag_traversal_manager.block_window(ghostdag_data.clone(), self.difficulty_window_size); - let (daa_score, daa_added_blocks) = self + let (daa_score, mergeset_non_daa) = self .difficulty_manager - .calc_daa_score_and_added_blocks(&mut window.iter().map(|item| item.0.hash), &ctx.ghostdag_data.clone().unwrap()); + .calc_daa_score_and_non_daa_mergeset_blocks(&mut window.iter().map(|item| item.0.hash), &ghostdag_data); if daa_score != header.daa_score { return Err(RuleError::UnexpectedHeaderDaaScore(daa_score, header.daa_score)); } - ctx.daa_added_blocks = Some(daa_added_blocks); + ctx.mergeset_non_daa = Some(mergeset_non_daa); let expected_bits = self.difficulty_manager.calculate_difficulty_bits(&window); if header.bits != expected_bits { diff --git a/consensus/src/pipeline/header_processor/processor.rs b/consensus/src/pipeline/header_processor/processor.rs index 4d612b11e..96e0e24ec 100644 --- a/consensus/src/pipeline/header_processor/processor.rs +++ b/consensus/src/pipeline/header_processor/processor.rs @@ -38,6 +38,7 @@ use crate::{ use consensus_core::{ blockhash::{BlockHashes, ORIGIN}, header::Header, + BlockHashSet, }; use crossbeam_channel::{Receiver, Sender}; use hashes::Hash; @@ -57,7 +58,7 @@ pub struct HeaderProcessingContext<'a> { pub ghostdag_data: Option>, pub block_window_for_difficulty: Option, pub block_window_for_past_median_time: Option, - pub daa_added_blocks: Option>, + pub mergeset_non_daa: Option, pub merge_depth_root: Option, pub finality_point: Option, pub block_level: Option, @@ -75,7 +76,7 @@ impl<'a> HeaderProcessingContext<'a> { ghostdag_data: None, non_pruned_parents: None, block_window_for_difficulty: None, - daa_added_blocks: None, + mergeset_non_daa: None, block_window_for_past_median_time: None, merge_depth_root: None, finality_point: None, @@ -319,7 +320,7 @@ impl HeaderProcessor { self.ghostdag_store.insert_batch(&mut batch, ctx.hash, &ghostdag_data).unwrap(); self.block_window_cache_for_difficulty.insert(ctx.hash, Arc::new(ctx.block_window_for_difficulty.unwrap())); self.block_window_cache_for_past_median_time.insert(ctx.hash, Arc::new(ctx.block_window_for_past_median_time.unwrap())); - self.daa_store.insert_batch(&mut batch, ctx.hash, Arc::new(ctx.daa_added_blocks.unwrap())).unwrap(); + self.daa_store.insert_batch(&mut batch, ctx.hash, Arc::new(ctx.mergeset_non_daa.unwrap())).unwrap(); self.headers_store.insert_batch(&mut batch, ctx.hash, ctx.header.clone(), ctx.block_level.unwrap()).unwrap(); self.depth_store.insert_batch(&mut batch, ctx.hash, ctx.merge_depth_root.unwrap(), ctx.finality_point.unwrap()).unwrap(); @@ -395,7 +396,7 @@ impl HeaderProcessor { ctx.ghostdag_data = Some(self.ghostdag_manager.genesis_ghostdag_data()); ctx.block_window_for_difficulty = Some(Default::default()); ctx.block_window_for_past_median_time = Some(Default::default()); - ctx.daa_added_blocks = Some(Default::default()); + ctx.mergeset_non_daa = Some(Default::default()); ctx.merge_depth_root = Some(ORIGIN); ctx.finality_point = Some(ORIGIN); ctx.block_level = Some(self.max_block_level); diff --git a/consensus/src/pipeline/virtual_processor/processor.rs b/consensus/src/pipeline/virtual_processor/processor.rs index ee2fbb67c..dd3216326 100644 --- a/consensus/src/pipeline/virtual_processor/processor.rs +++ b/consensus/src/pipeline/virtual_processor/processor.rs @@ -7,6 +7,7 @@ use crate::{ acceptance_data::{AcceptanceData, DbAcceptanceDataStore}, block_transactions::DbBlockTransactionsStore, block_window_cache::BlockWindowCacheStore, + daa::DbDaaStore, errors::StoreError, ghostdag::{DbGhostdagStore, GhostdagStoreReader}, headers::{DbHeadersStore, HeaderStoreReader}, @@ -28,8 +29,8 @@ use crate::{ params::Params, pipeline::{deps_manager::BlockTask, virtual_processor::utxo_validation::UtxoProcessingContext}, processes::{ - difficulty::DifficultyManager, pruning::PruningManager, transaction_validator::TransactionValidator, - traversal_manager::DagTraversalManager, + coinbase::CoinbaseManager, difficulty::DifficultyManager, pruning::PruningManager, + transaction_validator::TransactionValidator, traversal_manager::DagTraversalManager, }, }; use consensus_core::utxo::{utxo_diff::UtxoDiff, utxo_view::UtxoViewComposition}; @@ -65,6 +66,7 @@ pub struct VirtualStateProcessor { pub(super) statuses_store: Arc>, pub(super) ghostdag_store: Arc, pub(super) headers_store: Arc, + pub(super) daa_store: Arc, pub(super) block_transactions_store: Arc, pub(super) pruning_store: Arc>, pub(super) past_pruning_points_store: Arc, @@ -82,6 +84,7 @@ pub struct VirtualStateProcessor { pub(super) reachability_service: MTReachabilityService, pub(super) dag_traversal_manager: DagTraversalManager, pub(super) difficulty_manager: DifficultyManager, + pub(super) coinbase_manager: CoinbaseManager, pub(super) transaction_validator: TransactionValidator, pub(super) pruning_manager: PruningManager, } @@ -97,6 +100,7 @@ impl VirtualStateProcessor { statuses_store: Arc>, ghostdag_store: Arc, headers_store: Arc, + daa_store: Arc, block_transactions_store: Arc, pruning_store: Arc>, past_pruning_points_store: Arc, @@ -110,6 +114,7 @@ impl VirtualStateProcessor { reachability_service: MTReachabilityService, dag_traversal_manager: DagTraversalManager, difficulty_manager: DifficultyManager, + coinbase_manager: CoinbaseManager, transaction_validator: TransactionValidator, pruning_manager: PruningManager, ) -> Self { @@ -127,6 +132,7 @@ impl VirtualStateProcessor { statuses_store, headers_store, ghostdag_store, + daa_store, block_transactions_store, pruning_store, past_pruning_points_store, @@ -146,6 +152,7 @@ impl VirtualStateProcessor { reachability_service, dag_traversal_manager, difficulty_manager, + coinbase_manager, transaction_validator, pruning_manager, } @@ -184,7 +191,6 @@ impl VirtualStateProcessor { // TODO: check finality violation // TODO: handle disqualified chain loop - // TODO: coinbase validation // TODO: acceptance data format // TODO: refactor this methods into multiple methods @@ -262,13 +268,15 @@ impl VirtualStateProcessor { let window = self.dag_traversal_manager.block_window(virtual_ghostdag_data.clone(), self.difficulty_window_size); let (virtual_daa_score, _) = self .difficulty_manager - .calc_daa_score_and_added_blocks(&mut window.iter().map(|item| item.0.hash), &virtual_ghostdag_data); + .calc_daa_score_and_non_daa_mergeset_blocks(&mut window.iter().map(|item| item.0.hash), &virtual_ghostdag_data); self.calculate_utxo_state(&mut ctx, &selected_parent_utxo_view, virtual_daa_score); // Update the accumulated diff accumulated_diff.with_diff_in_place(&ctx.mergeset_diff).unwrap(); // Build the new virtual state + // TODO: store virtual mergeset fees and virtual mergeset non-DAA blocks in virtual state, + // so that virtual coinbase can be built (for build block template) let new_virtual_state = VirtualState::new(virtual_parents, virtual_ghostdag_data, virtual_daa_score, ctx.multiset_hash, ctx.mergeset_diff); diff --git a/consensus/src/pipeline/virtual_processor/utxo_validation.rs b/consensus/src/pipeline/virtual_processor/utxo_validation.rs index 810bb39fd..ffc969553 100644 --- a/consensus/src/pipeline/virtual_processor/utxo_validation.rs +++ b/consensus/src/pipeline/virtual_processor/utxo_validation.rs @@ -2,11 +2,13 @@ use super::VirtualStateProcessor; use crate::{ errors::{ BlockProcessResult, - RuleError::{BadAcceptedIDMerkleRoot, BadUTXOCommitment, InvalidTransactionsInUtxoContext}, + RuleError::{BadAcceptedIDMerkleRoot, BadCoinbaseTransaction, BadUTXOCommitment, InvalidTransactionsInUtxoContext}, }, - model::stores::{block_transactions::BlockTransactionsStoreReader, ghostdag::GhostdagData}, + model::stores::{block_transactions::BlockTransactionsStoreReader, daa::DaaStoreReader, ghostdag::GhostdagData}, + processes::coinbase::BlockRewardData, }; use consensus_core::{ + hashing, header::Header, muhash::MuHashExtensions, tx::{PopulatedTransaction, Transaction, TransactionId, ValidatedTransaction}, @@ -14,7 +16,7 @@ use consensus_core::{ utxo_diff::UtxoDiff, utxo_view::{UtxoView, UtxoViewComposition}, }, - BlockHashMap, HashMapCustomHasher, + BlockHashMap, BlockHashSet, HashMapCustomHasher, }; use hashes::Hash; use kaspa_core::trace; @@ -30,7 +32,7 @@ pub(super) struct UtxoProcessingContext { pub multiset_hash: MuHash, pub mergeset_diff: UtxoDiff, pub accepted_tx_ids: Vec, - pub mergeset_fees: BlockHashMap, + pub mergeset_rewards: BlockHashMap, } impl UtxoProcessingContext { @@ -41,7 +43,7 @@ impl UtxoProcessingContext { multiset_hash: selected_parent_multiset_hash, mergeset_diff: UtxoDiff::default(), accepted_tx_ids: Vec::with_capacity(1), // We expect at least the selected parent coinbase tx - mergeset_fees: BlockHashMap::with_capacity(mergeset_size), + mergeset_rewards: BlockHashMap::with_capacity(mergeset_size), } } @@ -86,7 +88,12 @@ impl VirtualStateProcessor { ctx.accepted_tx_ids.push(validated_tx.id()); block_fee += validated_tx.calculated_fee; } - ctx.mergeset_fees.insert(merged_block, block_fee); + + let coinbase_data = self.coinbase_manager.deserialize_coinbase_payload(&txs[0].payload).unwrap(); + ctx.mergeset_rewards.insert( + merged_block, + BlockRewardData::new(coinbase_data.subsidy, block_fee, coinbase_data.miner_data.script_public_key), + ); } } @@ -120,7 +127,13 @@ impl VirtualStateProcessor { let txs = self.block_transactions_store.get(header.hash).unwrap(); // Verify coinbase transaction - self.verify_coinbase_transaction(&txs[0], &ctx.ghostdag_data, &ctx.mergeset_fees)?; + self.verify_coinbase_transaction( + &txs[0], + header.daa_score, + &ctx.ghostdag_data, + &ctx.mergeset_rewards, + &self.daa_store.get_mergeset_non_daa(header.hash).unwrap(), + )?; // Verify all transactions are valid in context (TODO: skip validation when becoming selected parent) let current_utxo_view = selected_parent_utxo_view.compose(&ctx.mergeset_diff); @@ -135,13 +148,30 @@ impl VirtualStateProcessor { fn verify_coinbase_transaction( self: &Arc, - _coinbase_tx: &Transaction, - _mergeset_data: &GhostdagData, - _mergeset_fees: &BlockHashMap, + coinbase: &Transaction, + daa_score: u64, + ghostdag_data: &GhostdagData, + mergeset_rewards: &BlockHashMap, + mergeset_non_daa: &BlockHashSet, ) -> BlockProcessResult<()> { - // TODO: build expected coinbase using `mergeset_fees` and compare with the given tx - // Return `Err(BadCoinbaseTransaction)` if the expected and actual defer - Ok(()) + // Extract only miner data from the provided coinbase + let miner_data = self.coinbase_manager.deserialize_coinbase_payload(&coinbase.payload).unwrap().miner_data; + let expected_coinbase = self + .coinbase_manager + .expected_coinbase_transaction(daa_score, miner_data, ghostdag_data, mergeset_rewards, mergeset_non_daa) + .unwrap() + .tx; + trace!( + "mergeset: {} blues, {} reds, {} non-DAA", + ghostdag_data.mergeset_blues.len(), + ghostdag_data.mergeset_reds.len(), + mergeset_non_daa.len() + ); + if hashing::tx::hash(coinbase) != hashing::tx::hash(&expected_coinbase) { + Err(BadCoinbaseTransaction) + } else { + Ok(()) + } } /// Validates transactions against the provided `utxo_view` and returns a vector with all transactions diff --git a/consensus/src/processes/coinbase.rs b/consensus/src/processes/coinbase.rs index 0aa6441b3..87dcc792f 100644 --- a/consensus/src/processes/coinbase.rs +++ b/consensus/src/processes/coinbase.rs @@ -1,14 +1,20 @@ -use std::convert::TryInto; +use consensus_core::{ + subnets, + tx::{ScriptPublicKey, ScriptVec, Transaction, TransactionOutput}, + BlockHashMap, BlockHashSet, +}; +use std::{convert::TryInto, mem::size_of}; +use thiserror::Error; -use consensus_core::tx::{ScriptPublicKey, ScriptVec, Transaction}; +use crate::{constants, model::stores::ghostdag::GhostdagData}; -const UINT64_LEN: usize = 8; -const UINT16_LEN: usize = 2; -const LENGTH_OF_SUBSIDY: usize = UINT64_LEN; -const LENGTH_OF_SCRIPT_PUB_KEY_LENGTH: usize = 1; -const LENGTH_OF_VERSION_SCRIPT_PUB_KEY: usize = UINT16_LEN; +const LENGTH_OF_BLUE_SCORE: usize = size_of::(); +const LENGTH_OF_SUBSIDY: usize = size_of::(); +const LENGTH_OF_SCRIPT_PUB_KEY_VERSION: usize = size_of::(); +const LENGTH_OF_SCRIPT_PUB_KEY_LENGTH: usize = size_of::(); -use thiserror::Error; +const MIN_PAYLOAD_LENGTH: usize = + LENGTH_OF_BLUE_SCORE + LENGTH_OF_SUBSIDY + LENGTH_OF_SCRIPT_PUB_KEY_VERSION + LENGTH_OF_SCRIPT_PUB_KEY_LENGTH; #[derive(Error, Debug, Clone)] pub enum CoinbaseError { @@ -19,12 +25,9 @@ pub enum CoinbaseError { PayloadLenAboveMax(usize, usize), #[error("coinbase payload script public key length is {0} while the maximum allowed length is {1}")] - PayloadScriptPublicKeyLenAboveMax(u8, u8), - - #[error("coinbase payload script public key length is {0} while the maximum allowed length is {1}")] - PayloadScriptPublicKeyLen(u8, u8), + PayloadScriptPublicKeyLenAboveMax(usize, u8), - #[error("coinbase payload length is {0} bytes but it needs to be at least {1} bytes long in order of accomodating the script public key")] + #[error("coinbase payload length is {0} bytes but it needs to be at least {1} bytes long in order to accommodate the script public key")] PayloadCantContainScriptPublicKey(usize, usize), } @@ -38,17 +41,55 @@ pub struct CoinbaseManager { pre_deflationary_phase_base_subsidy: u64, } +#[derive(PartialEq, Eq, Debug)] pub struct CoinbaseData<'a> { pub blue_score: u64, pub subsidy: u64, pub miner_data: MinerData<'a>, } +#[derive(PartialEq, Eq, Debug, Clone)] pub struct MinerData<'a> { pub script_public_key: ScriptPublicKey, pub extra_data: &'a [u8], } +pub struct BlockRewardData { + pub subsidy: u64, + pub total_fees: u64, + pub script_public_key: ScriptPublicKey, +} + +impl BlockRewardData { + pub fn new(subsidy: u64, total_fees: u64, script_public_key: ScriptPublicKey) -> Self { + Self { subsidy, total_fees, script_public_key } + } +} + +/// Holds a coinbase transaction along with meta-data obtained during creation +pub struct CoinbaseTransactionTemplate { + pub tx: Transaction, + pub has_red_reward: bool, // Does the last output contain reward for red blocks +} + +/// Struct used to streamline payload parsing +struct PayloadParser<'a> { + remaining: &'a [u8], // The unparsed remainder +} + +impl<'a> PayloadParser<'a> { + fn new(data: &'a [u8]) -> Self { + Self { remaining: data } + } + + /// Returns a slice with the first `n` bytes of `remaining`, while setting `remaining` to the remaining part + fn take(&mut self, n: usize) -> &[u8] { + let (segment, remaining) = self.remaining.split_at(n); + self.remaining = remaining; + segment + } +} + impl CoinbaseManager { pub fn new( coinbase_payload_script_public_key_max_len: u8, @@ -64,62 +105,123 @@ impl CoinbaseManager { } } - pub fn validate_coinbase_payload_in_isolation_and_extract_coinbase_data<'a>( + pub fn expected_coinbase_transaction( &self, - coinbase: &'a Transaction, - ) -> CoinbaseResult> { - let payload = &coinbase.payload; - const MIN_LEN: usize = UINT64_LEN + LENGTH_OF_SUBSIDY + LENGTH_OF_VERSION_SCRIPT_PUB_KEY + LENGTH_OF_SCRIPT_PUB_KEY_LENGTH; + daa_score: u64, + miner_data: MinerData, + ghostdag_data: &GhostdagData, + mergeset_rewards: &BlockHashMap, + mergeset_non_daa: &BlockHashSet, + ) -> CoinbaseResult { + let mut outputs = Vec::with_capacity(ghostdag_data.mergeset_blues.len() + 1); // + 1 for possible red reward + + // Add an output for each mergeset blue block (∩ DAA window), paying to the script reported by the block. + // Note that combinatorically it is nearly impossible for a blue block to be non-DAA + for blue in ghostdag_data.mergeset_blues.iter().filter(|h| !mergeset_non_daa.contains(h)) { + let reward_data = mergeset_rewards.get(blue).unwrap(); + if reward_data.subsidy + reward_data.total_fees > 0 { + outputs + .push(TransactionOutput::new(reward_data.subsidy + reward_data.total_fees, reward_data.script_public_key.clone())); + } + } + + // Collect all rewards from mergeset reds ∩ DAA window and create a + // single output rewarding all to the current block (the "merging" block) + let mut red_reward = 0u64; + for red in ghostdag_data.mergeset_reds.iter().filter(|h| !mergeset_non_daa.contains(h)) { + let reward_data = mergeset_rewards.get(red).unwrap(); + red_reward += reward_data.subsidy + reward_data.total_fees; + } + if red_reward > 0 { + outputs.push(TransactionOutput::new(red_reward, miner_data.script_public_key.clone())); + } + + // Build the current block's payload + let subsidy = self.calc_block_subsidy(daa_score); + let payload = self.serialize_coinbase_payload(&CoinbaseData { blue_score: ghostdag_data.blue_score, subsidy, miner_data })?; + + Ok(CoinbaseTransactionTemplate { + tx: Transaction::new(constants::TX_VERSION, vec![], outputs, 0, subnets::SUBNETWORK_ID_COINBASE, 0, payload), + has_red_reward: red_reward > 0, + }) + } + + pub fn serialize_coinbase_payload(&self, data: &CoinbaseData) -> CoinbaseResult> { + let script_pub_key_len = data.miner_data.script_public_key.script().len(); + if script_pub_key_len > self.coinbase_payload_script_public_key_max_len as usize { + return Err(CoinbaseError::PayloadScriptPublicKeyLenAboveMax( + script_pub_key_len, + self.coinbase_payload_script_public_key_max_len, + )); + } + let payload: Vec = data.blue_score.to_le_bytes().iter().copied() // Blue score (u64) + .chain(data.subsidy.to_le_bytes().iter().copied()) // Subsidy (u64) + .chain(data.miner_data.script_public_key.version().to_le_bytes().iter().copied()) // Script public key version (u16) + .chain((script_pub_key_len as u8).to_le_bytes().iter().copied()) // Script public key length (u8) + .chain(data.miner_data.script_public_key.script().iter().copied()) // Script public key + .chain(data.miner_data.extra_data.iter().copied()) // Extra data + .collect(); + + Ok(payload) + } + + pub fn modify_coinbase_payload(&self, mut payload: Vec, miner_data: &MinerData) -> CoinbaseResult> { + let script_pub_key_len = miner_data.script_public_key.script().len(); + if script_pub_key_len > self.coinbase_payload_script_public_key_max_len as usize { + return Err(CoinbaseError::PayloadScriptPublicKeyLenAboveMax( + script_pub_key_len, + self.coinbase_payload_script_public_key_max_len, + )); + } + + // Keep only blue score and subsidy. Note that truncate does not modify capacity, so + // the usual case where the payloads are the same size will not trigger a reallocation + payload.truncate(LENGTH_OF_BLUE_SCORE + LENGTH_OF_SUBSIDY); + payload.extend( + miner_data.script_public_key.version().to_le_bytes().iter().copied() // Script public key version (u16) + .chain((script_pub_key_len as u8).to_le_bytes().iter().copied()) // Script public key length (u8) + .chain(miner_data.script_public_key.script().iter().copied()) // Script public key + .chain(miner_data.extra_data.iter().copied()), // Extra data + ); + + Ok(payload) + } - if payload.len() < MIN_LEN { - return Err(CoinbaseError::PayloadLenBelowMin(coinbase.payload.len(), MIN_LEN)); + pub fn deserialize_coinbase_payload<'a>(&self, payload: &'a [u8]) -> CoinbaseResult> { + if payload.len() < MIN_PAYLOAD_LENGTH { + return Err(CoinbaseError::PayloadLenBelowMin(payload.len(), MIN_PAYLOAD_LENGTH)); } if payload.len() > self.max_coinbase_payload_len { - return Err(CoinbaseError::PayloadLenAboveMax(coinbase.payload.len(), self.max_coinbase_payload_len)); + return Err(CoinbaseError::PayloadLenAboveMax(payload.len(), self.max_coinbase_payload_len)); } - let blue_score = u64::from_le_bytes(payload[..UINT64_LEN].try_into().unwrap()); - let subsidy = u64::from_le_bytes(payload[UINT64_LEN..UINT64_LEN + LENGTH_OF_SUBSIDY].try_into().unwrap()); + let mut parser = PayloadParser::new(payload); - const VERSION_START: usize = UINT64_LEN + LENGTH_OF_SUBSIDY; - let script_pub_key_version = - u16::from_le_bytes(payload[VERSION_START..VERSION_START + LENGTH_OF_VERSION_SCRIPT_PUB_KEY].try_into().unwrap()); + let blue_score = u64::from_le_bytes(parser.take(LENGTH_OF_BLUE_SCORE).try_into().unwrap()); + let subsidy = u64::from_le_bytes(parser.take(LENGTH_OF_SUBSIDY).try_into().unwrap()); + let script_pub_key_version = u16::from_le_bytes(parser.take(LENGTH_OF_SCRIPT_PUB_KEY_VERSION).try_into().unwrap()); + let script_pub_key_len = u8::from_le_bytes(parser.take(LENGTH_OF_SCRIPT_PUB_KEY_LENGTH).try_into().unwrap()); - let script_pub_key_len = payload[UINT64_LEN + LENGTH_OF_SUBSIDY + LENGTH_OF_VERSION_SCRIPT_PUB_KEY]; if script_pub_key_len > self.coinbase_payload_script_public_key_max_len { return Err(CoinbaseError::PayloadScriptPublicKeyLenAboveMax( - script_pub_key_len, + script_pub_key_len as usize, self.coinbase_payload_script_public_key_max_len, )); } - if payload.len() < MIN_LEN + script_pub_key_len as usize { - return Err(CoinbaseError::PayloadCantContainScriptPublicKey(payload.len(), script_pub_key_len as usize)); + if parser.remaining.len() < script_pub_key_len as usize { + return Err(CoinbaseError::PayloadCantContainScriptPublicKey( + payload.len(), + MIN_PAYLOAD_LENGTH + script_pub_key_len as usize, + )); } - let script_pub_key_script = - &payload[UINT64_LEN + LENGTH_OF_SUBSIDY + LENGTH_OF_VERSION_SCRIPT_PUB_KEY + LENGTH_OF_SCRIPT_PUB_KEY_LENGTH - ..UINT64_LEN - + LENGTH_OF_SUBSIDY - + LENGTH_OF_VERSION_SCRIPT_PUB_KEY - + LENGTH_OF_SCRIPT_PUB_KEY_LENGTH - + script_pub_key_len as usize]; - - let extra_data = &payload[UINT64_LEN - + LENGTH_OF_SUBSIDY - + LENGTH_OF_VERSION_SCRIPT_PUB_KEY - + LENGTH_OF_SCRIPT_PUB_KEY_LENGTH - + script_pub_key_len as usize..]; - - Ok(CoinbaseData { - blue_score, - subsidy, - miner_data: MinerData { - script_public_key: ScriptPublicKey::new(script_pub_key_version, ScriptVec::from_slice(script_pub_key_script)), - extra_data, - }, - }) + let script_public_key = + ScriptPublicKey::new(script_pub_key_version, ScriptVec::from_slice(parser.take(script_pub_key_len as usize))); + let extra_data = parser.remaining; + + Ok(CoinbaseData { blue_score, subsidy, miner_data: MinerData { script_public_key, extra_data } }) } pub fn calc_block_subsidy(&self, daa_score: u64) -> u64 { @@ -144,443 +246,36 @@ impl CoinbaseManager { } /* - This table was pre-calculated by calling `calcDeflationaryPeriodBlockSubsidyFloatCalc` for all months until reaching 0 subsidy. + This table was pre-calculated by calling `calcDeflationaryPeriodBlockSubsidyFloatCalc` (in kaspad-go) for all months until reaching 0 subsidy. To regenerate this table, run `TestBuildSubsidyTable` in coinbasemanager_test.go (note the `deflationaryPhaseBaseSubsidy` therein) */ +#[rustfmt::skip] const SUBSIDY_BY_MONTH_TABLE: [u64; 426] = [ - 44000000000, - 41530469757, - 39199543598, - 36999442271, - 34922823143, - 32962755691, - 31112698372, - 29366476791, - 27718263097, - 26162556530, - 24694165062, - 23308188075, - 22000000000, - 20765234878, - 19599771799, - 18499721135, - 17461411571, - 16481377845, - 15556349186, - 14683238395, - 13859131548, - 13081278265, - 12347082531, - 11654094037, - 11000000000, - 10382617439, - 9799885899, - 9249860567, - 8730705785, - 8240688922, - 7778174593, - 7341619197, - 6929565774, - 6540639132, - 6173541265, - 5827047018, - 5500000000, - 5191308719, - 4899942949, - 4624930283, - 4365352892, - 4120344461, - 3889087296, - 3670809598, - 3464782887, - 3270319566, - 3086770632, - 2913523509, - 2750000000, - 2595654359, - 2449971474, - 2312465141, - 2182676446, - 2060172230, - 1944543648, - 1835404799, - 1732391443, - 1635159783, - 1543385316, - 1456761754, - 1375000000, - 1297827179, - 1224985737, - 1156232570, - 1091338223, - 1030086115, - 972271824, - 917702399, - 866195721, - 817579891, - 771692658, - 728380877, - 687500000, - 648913589, - 612492868, - 578116285, - 545669111, - 515043057, - 486135912, - 458851199, - 433097860, - 408789945, - 385846329, - 364190438, - 343750000, - 324456794, - 306246434, - 289058142, - 272834555, - 257521528, - 243067956, - 229425599, - 216548930, - 204394972, - 192923164, - 182095219, - 171875000, - 162228397, - 153123217, - 144529071, - 136417277, - 128760764, - 121533978, - 114712799, - 108274465, - 102197486, - 96461582, - 91047609, - 85937500, - 81114198, - 76561608, - 72264535, - 68208638, - 64380382, - 60766989, - 57356399, - 54137232, - 51098743, - 48230791, - 45523804, - 42968750, - 40557099, - 38280804, - 36132267, - 34104319, - 32190191, - 30383494, - 28678199, - 27068616, - 25549371, - 24115395, - 22761902, - 21484375, - 20278549, - 19140402, - 18066133, - 17052159, - 16095095, - 15191747, - 14339099, - 13534308, - 12774685, - 12057697, - 11380951, - 10742187, - 10139274, - 9570201, - 9033066, - 8526079, - 8047547, - 7595873, - 7169549, - 6767154, - 6387342, - 6028848, - 5690475, - 5371093, - 5069637, - 4785100, - 4516533, - 4263039, - 4023773, - 3797936, - 3584774, - 3383577, - 3193671, - 3014424, - 2845237, - 2685546, - 2534818, - 2392550, - 2258266, - 2131519, - 2011886, - 1898968, - 1792387, - 1691788, - 1596835, - 1507212, - 1422618, - 1342773, - 1267409, - 1196275, - 1129133, - 1065759, - 1005943, - 949484, - 896193, - 845894, - 798417, - 753606, - 711309, - 671386, - 633704, - 598137, - 564566, - 532879, - 502971, - 474742, - 448096, - 422947, - 399208, - 376803, - 355654, - 335693, - 316852, - 299068, - 282283, - 266439, - 251485, - 237371, - 224048, - 211473, - 199604, - 188401, - 177827, - 167846, - 158426, - 149534, - 141141, - 133219, - 125742, - 118685, - 112024, - 105736, - 99802, - 94200, - 88913, - 83923, - 79213, - 74767, - 70570, - 66609, - 62871, - 59342, - 56012, - 52868, - 49901, - 47100, - 44456, - 41961, - 39606, - 37383, - 35285, - 33304, - 31435, - 29671, - 28006, - 26434, - 24950, - 23550, - 22228, - 20980, - 19803, - 18691, - 17642, - 16652, - 15717, - 14835, - 14003, - 13217, - 12475, - 11775, - 11114, - 10490, - 9901, - 9345, - 8821, - 8326, - 7858, - 7417, - 7001, - 6608, - 6237, - 5887, - 5557, - 5245, - 4950, - 4672, - 4410, - 4163, - 3929, - 3708, - 3500, - 3304, - 3118, - 2943, - 2778, - 2622, - 2475, - 2336, - 2205, - 2081, - 1964, - 1854, - 1750, - 1652, - 1559, - 1471, - 1389, - 1311, - 1237, - 1168, - 1102, - 1040, - 982, - 927, - 875, - 826, - 779, - 735, - 694, - 655, - 618, - 584, - 551, - 520, - 491, - 463, - 437, - 413, - 389, - 367, - 347, - 327, - 309, - 292, - 275, - 260, - 245, - 231, - 218, - 206, - 194, - 183, - 173, - 163, - 154, - 146, - 137, - 130, - 122, - 115, - 109, - 103, - 97, - 91, - 86, - 81, - 77, - 73, - 68, - 65, - 61, - 57, - 54, - 51, - 48, - 45, - 43, - 40, - 38, - 36, - 34, - 32, - 30, - 28, - 27, - 25, - 24, - 22, - 21, - 20, - 19, - 18, - 17, - 16, - 15, - 14, - 13, - 12, - 12, - 11, - 10, - 10, - 9, - 9, - 8, - 8, - 7, - 7, - 6, - 6, - 6, - 5, - 5, - 5, - 4, - 4, - 4, - 4, - 3, - 3, - 3, - 3, - 3, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 0, + 44000000000, 41530469757, 39199543598, 36999442271, 34922823143, 32962755691, 31112698372, 29366476791, 27718263097, 26162556530, 24694165062, 23308188075, 22000000000, 20765234878, 19599771799, 18499721135, 17461411571, 16481377845, 15556349186, 14683238395, 13859131548, 13081278265, 12347082531, 11654094037, 11000000000, + 10382617439, 9799885899, 9249860567, 8730705785, 8240688922, 7778174593, 7341619197, 6929565774, 6540639132, 6173541265, 5827047018, 5500000000, 5191308719, 4899942949, 4624930283, 4365352892, 4120344461, 3889087296, 3670809598, 3464782887, 3270319566, 3086770632, 2913523509, 2750000000, 2595654359, + 2449971474, 2312465141, 2182676446, 2060172230, 1944543648, 1835404799, 1732391443, 1635159783, 1543385316, 1456761754, 1375000000, 1297827179, 1224985737, 1156232570, 1091338223, 1030086115, 972271824, 917702399, 866195721, 817579891, 771692658, 728380877, 687500000, 648913589, 612492868, + 578116285, 545669111, 515043057, 486135912, 458851199, 433097860, 408789945, 385846329, 364190438, 343750000, 324456794, 306246434, 289058142, 272834555, 257521528, 243067956, 229425599, 216548930, 204394972, 192923164, 182095219, 171875000, 162228397, 153123217, 144529071, + 136417277, 128760764, 121533978, 114712799, 108274465, 102197486, 96461582, 91047609, 85937500, 81114198, 76561608, 72264535, 68208638, 64380382, 60766989, 57356399, 54137232, 51098743, 48230791, 45523804, 42968750, 40557099, 38280804, 36132267, 34104319, + 32190191, 30383494, 28678199, 27068616, 25549371, 24115395, 22761902, 21484375, 20278549, 19140402, 18066133, 17052159, 16095095, 15191747, 14339099, 13534308, 12774685, 12057697, 11380951, 10742187, 10139274, 9570201, 9033066, 8526079, 8047547, + 7595873, 7169549, 6767154, 6387342, 6028848, 5690475, 5371093, 5069637, 4785100, 4516533, 4263039, 4023773, 3797936, 3584774, 3383577, 3193671, 3014424, 2845237, 2685546, 2534818, 2392550, 2258266, 2131519, 2011886, 1898968, + 1792387, 1691788, 1596835, 1507212, 1422618, 1342773, 1267409, 1196275, 1129133, 1065759, 1005943, 949484, 896193, 845894, 798417, 753606, 711309, 671386, 633704, 598137, 564566, 532879, 502971, 474742, 448096, + 422947, 399208, 376803, 355654, 335693, 316852, 299068, 282283, 266439, 251485, 237371, 224048, 211473, 199604, 188401, 177827, 167846, 158426, 149534, 141141, 133219, 125742, 118685, 112024, 105736, + 99802, 94200, 88913, 83923, 79213, 74767, 70570, 66609, 62871, 59342, 56012, 52868, 49901, 47100, 44456, 41961, 39606, 37383, 35285, 33304, 31435, 29671, 28006, 26434, 24950, + 23550, 22228, 20980, 19803, 18691, 17642, 16652, 15717, 14835, 14003, 13217, 12475, 11775, 11114, 10490, 9901, 9345, 8821, 8326, 7858, 7417, 7001, 6608, 6237, 5887, + 5557, 5245, 4950, 4672, 4410, 4163, 3929, 3708, 3500, 3304, 3118, 2943, 2778, 2622, 2475, 2336, 2205, 2081, 1964, 1854, 1750, 1652, 1559, 1471, 1389, + 1311, 1237, 1168, 1102, 1040, 982, 927, 875, 826, 779, 735, 694, 655, 618, 584, 551, 520, 491, 463, 437, 413, 389, 367, 347, 327, + 309, 292, 275, 260, 245, 231, 218, 206, 194, 183, 173, 163, 154, 146, 137, 130, 122, 115, 109, 103, 97, 91, 86, 81, 77, + 73, 68, 65, 61, 57, 54, 51, 48, 45, 43, 40, 38, 36, 34, 32, 30, 28, 27, 25, 24, 22, 21, 20, 19, 18, + 17, 16, 15, 14, 13, 12, 12, 11, 10, 10, 9, 9, 8, 8, 7, 7, 6, 6, 6, 5, 5, 5, 4, 4, 4, + 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 0, ]; #[cfg(test)] mod tests { + use super::*; use crate::params::MAINNET_PARAMS; - - use super::CoinbaseManager; + use consensus_core::tx::scriptvec; #[test] fn subsidy_test() { @@ -649,4 +344,92 @@ mod tests { assert_eq!(cbm.calc_block_subsidy(t.daa_score), t.expected, "test '{}' failed", t.name); } } + + #[test] + fn payload_serialization_test() { + let params = &MAINNET_PARAMS; + let cbm = CoinbaseManager::new( + params.coinbase_payload_script_public_key_max_len, + params.max_coinbase_payload_len, + params.deflationary_phase_daa_score, + params.pre_deflationary_phase_base_subsidy, + ); + + let script_data = [33u8, 255]; + let extra_data = [2u8, 3]; + let data = CoinbaseData { + blue_score: 56, + subsidy: 44000000000, + miner_data: MinerData { + script_public_key: ScriptPublicKey::new(0, ScriptVec::from_slice(&script_data)), + extra_data: &extra_data, + }, + }; + + let payload = cbm.serialize_coinbase_payload(&data).unwrap(); + let deserialized_data = cbm.deserialize_coinbase_payload(&payload).unwrap(); + + assert_eq!(data, deserialized_data); + + // Test an actual mainnet payload + let payload_hex = + "b612c90100000000041a763e07000000000022202b32443ff740012157716d81216d09aebc39e5493c93a7181d92cb756c02c560ac302e31322e382f"; + let mut payload = vec![0u8; payload_hex.len() / 2]; + faster_hex::hex_decode(payload_hex.as_bytes(), &mut payload).unwrap(); + let deserialized_data = cbm.deserialize_coinbase_payload(&payload).unwrap(); + + let expected_data = CoinbaseData { + blue_score: 29954742, + subsidy: 31112698372, + miner_data: MinerData { + script_public_key: ScriptPublicKey::new( + 0, + scriptvec![ + 32, 43, 50, 68, 63, 247, 64, 1, 33, 87, 113, 109, 129, 33, 109, 9, 174, 188, 57, 229, 73, 60, 147, 167, 24, + 29, 146, 203, 117, 108, 2, 197, 96, 172, + ], + ), + extra_data: &[48, 46, 49, 50, 46, 56, 47], + }, + }; + assert_eq!(expected_data, deserialized_data); + } + + #[test] + fn modify_payload_test() { + let params = &MAINNET_PARAMS; + let cbm = CoinbaseManager::new( + params.coinbase_payload_script_public_key_max_len, + params.max_coinbase_payload_len, + params.deflationary_phase_daa_score, + params.pre_deflationary_phase_base_subsidy, + ); + + let script_data = [33u8, 255]; + let extra_data = [2u8, 3, 23, 98]; + let data = CoinbaseData { + blue_score: 56345, + subsidy: 44000000000, + miner_data: MinerData { + script_public_key: ScriptPublicKey::new(0, ScriptVec::from_slice(&script_data)), + extra_data: &extra_data, + }, + }; + + let data2 = CoinbaseData { + blue_score: data.blue_score, + subsidy: data.subsidy, + miner_data: MinerData { + // Modify only miner data + script_public_key: ScriptPublicKey::new(0, ScriptVec::from_slice(&[33u8, 255, 33])), + extra_data: &[2u8, 3, 23, 98, 34, 34], + }, + }; + + let mut payload = cbm.serialize_coinbase_payload(&data).unwrap(); + payload = cbm.modify_coinbase_payload(payload, &data2.miner_data).unwrap(); // Update the payload with the modified miner data + let deserialized_data = cbm.deserialize_coinbase_payload(&payload).unwrap(); + + assert_eq!(data2, deserialized_data); + } } diff --git a/consensus/src/processes/difficulty.rs b/consensus/src/processes/difficulty.rs index ce53338ea..17efb132b 100644 --- a/consensus/src/processes/difficulty.rs +++ b/consensus/src/processes/difficulty.rs @@ -28,21 +28,17 @@ impl DifficultyManager { Self { headers_store, difficulty_adjustment_window_size, genesis_bits, target_time_per_block } } - pub fn calc_daa_score_and_added_blocks( + pub fn calc_daa_score_and_non_daa_mergeset_blocks( &self, window_hashes: &mut impl ExactSizeIterator, ghostdag_data: &GhostdagData, - ) -> (u64, Vec) { - if window_hashes.len() == 0 { - return (0, Vec::new()); - } - - let mergeset_len = ghostdag_data.mergeset_size(); + ) -> (u64, BlockHashSet) { let mergeset: BlockHashSet = ghostdag_data.unordered_mergeset().collect(); - let daa_added_blocks: Vec<_> = window_hashes.filter(|h| mergeset.contains(h)).take(mergeset_len).collect(); + let mergeset_daa: BlockHashSet = window_hashes.filter(|h| mergeset.contains(h)).take(mergeset.len()).collect(); + let mergeset_non_daa: BlockHashSet = mergeset.difference(&mergeset_daa).copied().collect(); let sp_daa_score = self.headers_store.get_daa_score(ghostdag_data.selected_parent).unwrap(); - (sp_daa_score + daa_added_blocks.len() as u64, daa_added_blocks) + (sp_daa_score + mergeset_daa.len() as u64, mergeset_non_daa) } pub fn calculate_difficulty_bits(&self, window: &BlockWindowHeap) -> u32 {