use super::VirtualStateProcessor;
use crate::{
errors::{
BlockProcessResult,
RuleError::{BadAcceptedIDMerkleRoot, BadCoinbaseTransaction, BadUTXOCommitment, InvalidTransactionsInUtxoContext},
},
model::stores::{block_transactions::BlockTransactionsStoreReader, daa::DaaStoreReader, ghostdag::GhostdagData},
processes::transaction_validator::errors::{TxResult, TxRuleError},
};
use kaspa_consensus_core::{
acceptance_data::{AcceptedTxEntry, MergesetBlockAcceptanceData},
coinbase::*,
hashing,
header::Header,
muhash::MuHashExtensions,
tx::{MutableTransaction, PopulatedTransaction, Transaction, TransactionId, ValidatedTransaction, VerifiableTransaction},
utxo::{
utxo_diff::UtxoDiff,
utxo_view::{UtxoView, UtxoViewComposition},
},
BlockHashMap, BlockHashSet, HashMapCustomHasher,
};
use kaspa_core::{info, trace};
use kaspa_hashes::Hash;
use kaspa_muhash::MuHash;
use kaspa_utils::refs::Refs;
use rayon::prelude::*;
use std::{iter::once, ops::Deref};
pub(super) struct UtxoProcessingContext<'a> {
pub ghostdag_data: Refs<'a, GhostdagData>,
pub multiset_hash: MuHash,
pub mergeset_diff: UtxoDiff,
pub accepted_tx_ids: Vec<TransactionId>,
pub mergeset_acceptance_data: Vec<MergesetBlockAcceptanceData>,
pub mergeset_rewards: BlockHashMap<BlockRewardData>,
}
impl<'a> UtxoProcessingContext<'a> {
pub fn new(ghostdag_data: Refs<'a, GhostdagData>, selected_parent_multiset_hash: MuHash) -> Self {
let mergeset_size = ghostdag_data.mergeset_size();
Self {
ghostdag_data,
multiset_hash: selected_parent_multiset_hash,
mergeset_diff: UtxoDiff::default(),
accepted_tx_ids: Vec::with_capacity(1), mergeset_rewards: BlockHashMap::with_capacity(mergeset_size),
mergeset_acceptance_data: Vec::with_capacity(mergeset_size),
}
}
pub fn selected_parent(&self) -> Hash {
self.ghostdag_data.selected_parent
}
}
impl VirtualStateProcessor {
pub(super) fn calculate_utxo_state<V: UtxoView + Sync>(
&self,
ctx: &mut UtxoProcessingContext,
selected_parent_utxo_view: &V,
pov_daa_score: u64,
) {
let selected_parent_transactions = self.block_transactions_store.get(ctx.selected_parent()).unwrap();
let validated_coinbase = ValidatedTransaction::new_coinbase(&selected_parent_transactions[0]);
ctx.mergeset_diff.add_transaction(&validated_coinbase, pov_daa_score).unwrap();
ctx.multiset_hash.add_transaction(&validated_coinbase, pov_daa_score);
ctx.accepted_tx_ids.push(validated_coinbase.id());
for (merged_block, txs) in once((ctx.selected_parent(), selected_parent_transactions)).chain(
ctx.ghostdag_data
.consensus_ordered_mergeset_without_selected_parent(self.ghostdag_store.deref())
.map(|b| (b, self.block_transactions_store.get(b).unwrap())),
) {
let composed_view = selected_parent_utxo_view.compose(&ctx.mergeset_diff);
let validated_transactions = self.validate_transactions_in_parallel(&txs, &composed_view, pov_daa_score);
let mut block_fee = 0u64;
for (validated_tx, _) in validated_transactions.iter() {
ctx.mergeset_diff.add_transaction(validated_tx, pov_daa_score).unwrap();
ctx.multiset_hash.add_transaction(validated_tx, pov_daa_score);
ctx.accepted_tx_ids.push(validated_tx.id());
block_fee += validated_tx.calculated_fee;
}
ctx.mergeset_acceptance_data.push(MergesetBlockAcceptanceData {
block_hash: merged_block,
accepted_transactions: validated_transactions
.into_iter()
.map(|(tx, tx_idx)| AcceptedTxEntry { transaction_id: tx.id(), index_within_block: tx_idx })
.collect(),
});
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),
);
}
ctx.accepted_tx_ids.sort();
}
pub(super) fn verify_expected_utxo_state<V: UtxoView + Sync>(
&self,
ctx: &mut UtxoProcessingContext,
selected_parent_utxo_view: &V,
header: &Header,
) -> BlockProcessResult<()> {
let expected_commitment = ctx.multiset_hash.finalize();
if expected_commitment != header.utxo_commitment {
return Err(BadUTXOCommitment(header.hash, header.utxo_commitment, expected_commitment));
}
trace!("correct commitment: {}, {}", header.hash, expected_commitment);
let expected_accepted_id_merkle_root = kaspa_merkle::calc_merkle_root(ctx.accepted_tx_ids.iter().copied());
if expected_accepted_id_merkle_root != header.accepted_id_merkle_root {
return Err(BadAcceptedIDMerkleRoot(header.hash, header.accepted_id_merkle_root, expected_accepted_id_merkle_root));
}
let txs = self.block_transactions_store.get(header.hash).unwrap();
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(),
)?;
let current_utxo_view = selected_parent_utxo_view.compose(&ctx.mergeset_diff);
let validated_transactions = self.validate_transactions_in_parallel(&txs, ¤t_utxo_view, header.daa_score);
if validated_transactions.len() < txs.len() - 1 {
return Err(InvalidTransactionsInUtxoContext(txs.len() - 1 - validated_transactions.len(), txs.len() - 1));
}
Ok(())
}
fn verify_coinbase_transaction(
&self,
coinbase: &Transaction,
daa_score: u64,
ghostdag_data: &GhostdagData,
mergeset_rewards: &BlockHashMap<BlockRewardData>,
mergeset_non_daa: &BlockHashSet,
) -> BlockProcessResult<()> {
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;
if hashing::tx::hash(coinbase) != hashing::tx::hash(&expected_coinbase) {
Err(BadCoinbaseTransaction)
} else {
Ok(())
}
}
pub fn validate_transactions_in_parallel<'a, V: UtxoView + Sync>(
&self,
txs: &'a Vec<Transaction>,
utxo_view: &V,
pov_daa_score: u64,
) -> Vec<(ValidatedTransaction<'a>, u32)> {
self.thread_pool.install(|| {
txs
.par_iter() .enumerate()
.skip(1) .filter_map(|(i, tx)| self.validate_transaction_in_utxo_context(tx, &utxo_view, pov_daa_score).ok().map(|vtx| (vtx, i as u32)))
.collect()
})
}
pub(super) fn validate_transaction_in_utxo_context<'a>(
&self,
transaction: &'a Transaction,
utxo_view: &impl UtxoView,
pov_daa_score: u64,
) -> TxResult<ValidatedTransaction<'a>> {
let mut entries = Vec::with_capacity(transaction.inputs.len());
for input in transaction.inputs.iter() {
if let Some(entry) = utxo_view.get(&input.previous_outpoint) {
entries.push(entry);
} else {
return Err(TxRuleError::MissingTxOutpoints);
}
}
let populated_tx = PopulatedTransaction::new(transaction, entries);
let res = self.transaction_validator.validate_populated_transaction_and_get_fee(&populated_tx, pov_daa_score);
match res {
Ok(calculated_fee) => Ok(ValidatedTransaction::new(populated_tx, calculated_fee)),
Err(tx_rule_error) => {
info!("Rejecting transaction {} due to transaction rule error: {}", transaction.id(), tx_rule_error);
Err(tx_rule_error)
}
}
}
pub(super) fn validate_mempool_transaction_in_utxo_context(
&self,
mutable_tx: &mut MutableTransaction,
utxo_view: &impl UtxoView,
pov_daa_score: u64,
) -> TxResult<()> {
let mut has_missing_outpoints = false;
for i in 0..mutable_tx.tx.inputs.len() {
if mutable_tx.entries[i].is_some() {
continue;
}
if let Some(entry) = utxo_view.get(&mutable_tx.tx.inputs[i].previous_outpoint) {
mutable_tx.entries[i] = Some(entry);
} else {
has_missing_outpoints = true;
}
}
if has_missing_outpoints {
return Err(TxRuleError::MissingTxOutpoints);
}
let calculated_fee =
self.transaction_validator.validate_populated_transaction_and_get_fee(&mutable_tx.as_verifiable(), pov_daa_score)?;
mutable_tx.calculated_fee = Some(calculated_fee);
Ok(())
}
}