kaspa_consensus/pipeline/body_processor/
body_validation_in_context.rs

1use super::BlockBodyProcessor;
2use crate::{
3    errors::{BlockProcessResult, RuleError},
4    model::stores::{ghostdag::GhostdagStoreReader, statuses::StatusesStoreReader},
5    processes::window::WindowManager,
6};
7use kaspa_consensus_core::block::Block;
8use kaspa_database::prelude::StoreResultExtensions;
9use kaspa_hashes::Hash;
10use kaspa_utils::option::OptionExtensions;
11use std::sync::Arc;
12
13impl BlockBodyProcessor {
14    pub fn validate_body_in_context(self: &Arc<Self>, block: &Block) -> BlockProcessResult<()> {
15        self.check_parent_bodies_exist(block)?;
16        self.check_coinbase_blue_score_and_subsidy(block)?;
17        self.check_block_transactions_in_context(block)
18    }
19
20    fn check_block_transactions_in_context(self: &Arc<Self>, block: &Block) -> BlockProcessResult<()> {
21        let (pmt, _) = self.window_manager.calc_past_median_time(&self.ghostdag_store.get_data(block.hash()).unwrap())?;
22        for tx in block.transactions.iter() {
23            if let Err(e) = self.transaction_validator.utxo_free_tx_validation(tx, block.header.daa_score, pmt) {
24                return Err(RuleError::TxInContextFailed(tx.id(), e));
25            }
26        }
27
28        Ok(())
29    }
30
31    fn check_parent_bodies_exist(self: &Arc<Self>, block: &Block) -> BlockProcessResult<()> {
32        let statuses_read_guard = self.statuses_store.read();
33        let missing: Vec<Hash> = block
34            .header
35            .direct_parents()
36            .iter()
37            .copied()
38            .filter(|parent| {
39                let status_option = statuses_read_guard.get(*parent).unwrap_option();
40                status_option.is_none_or_ex(|s| !s.has_block_body())
41            })
42            .collect();
43        if !missing.is_empty() {
44            return Err(RuleError::MissingParents(missing));
45        }
46
47        Ok(())
48    }
49
50    fn check_coinbase_blue_score_and_subsidy(self: &Arc<Self>, block: &Block) -> BlockProcessResult<()> {
51        match self.coinbase_manager.deserialize_coinbase_payload(&block.transactions[0].payload) {
52            Ok(data) => {
53                if data.blue_score != block.header.blue_score {
54                    return Err(RuleError::BadCoinbasePayloadBlueScore(data.blue_score, block.header.blue_score));
55                }
56
57                let expected_subsidy = self.coinbase_manager.calc_block_subsidy(block.header.daa_score);
58
59                if data.subsidy != expected_subsidy {
60                    return Err(RuleError::WrongSubsidy(expected_subsidy, data.subsidy));
61                }
62
63                Ok(())
64            }
65            Err(e) => Err(RuleError::BadCoinbasePayload(e)),
66        }
67    }
68}
69
70#[cfg(test)]
71mod tests {
72
73    use crate::{
74        config::ConfigBuilder,
75        consensus::test_consensus::TestConsensus,
76        constants::TX_VERSION,
77        errors::RuleError,
78        model::stores::ghostdag::GhostdagStoreReader,
79        params::DEVNET_PARAMS,
80        processes::{transaction_validator::errors::TxRuleError, window::WindowManager},
81    };
82    use kaspa_consensus_core::{
83        api::ConsensusApi,
84        merkle::calc_hash_merkle_root as calc_hash_merkle_root_with_options,
85        subnets::SUBNETWORK_ID_NATIVE,
86        tx::{Transaction, TransactionInput, TransactionOutpoint},
87    };
88    use kaspa_core::assert_match;
89    use kaspa_hashes::Hash;
90
91    fn calc_hash_merkle_root<'a>(txs: impl ExactSizeIterator<Item = &'a Transaction>) -> Hash {
92        calc_hash_merkle_root_with_options(txs, false)
93    }
94
95    #[tokio::test]
96    async fn validate_body_in_context_test() {
97        let config = ConfigBuilder::new(DEVNET_PARAMS)
98            .skip_proof_of_work()
99            .edit_consensus_params(|p| p.deflationary_phase_daa_score = 2)
100            .build();
101        let consensus = TestConsensus::new(&config);
102        let wait_handles = consensus.init();
103        let body_processor = consensus.block_body_processor();
104
105        consensus.add_block_with_parents(1.into(), vec![config.genesis.hash]).await.unwrap();
106
107        {
108            let block = consensus.build_block_with_parents_and_transactions(2.into(), vec![1.into()], vec![]);
109            // We expect a missing parents error since the parent is header only.
110            assert_match!(body_processor.validate_body_in_context(&block.to_immutable()), Err(RuleError::MissingParents(_)));
111        }
112
113        let valid_block = consensus.build_block_with_parents_and_transactions(3.into(), vec![config.genesis.hash], vec![]);
114        consensus.validate_and_insert_block(valid_block.to_immutable()).virtual_state_task.await.unwrap();
115        {
116            let mut block = consensus.build_block_with_parents_and_transactions(2.into(), vec![3.into()], vec![]);
117            block.transactions[0].payload[8..16].copy_from_slice(&(5_u64).to_le_bytes());
118            block.header.hash_merkle_root = calc_hash_merkle_root(block.transactions.iter());
119
120            assert_match!(
121                consensus.validate_and_insert_block(block.clone().to_immutable()).virtual_state_task.await, Err(RuleError::WrongSubsidy(expected,_)) if expected == 50000000000);
122
123            // The second time we send an invalid block we expect it to be a known invalid.
124            assert_match!(
125                consensus.validate_and_insert_block(block.to_immutable()).virtual_state_task.await,
126                Err(RuleError::KnownInvalid)
127            );
128        }
129
130        {
131            let mut block = consensus.build_block_with_parents_and_transactions(4.into(), vec![3.into()], vec![]);
132            block.transactions[0].payload[0..8].copy_from_slice(&(100_u64).to_le_bytes());
133            block.header.hash_merkle_root = calc_hash_merkle_root(block.transactions.iter());
134
135            assert_match!(
136                consensus.validate_and_insert_block(block.to_immutable()).virtual_state_task.await,
137                Err(RuleError::BadCoinbasePayloadBlueScore(_, _))
138            );
139        }
140
141        {
142            let mut block = consensus.build_block_with_parents_and_transactions(5.into(), vec![3.into()], vec![]);
143            block.transactions[0].payload = vec![];
144            block.header.hash_merkle_root = calc_hash_merkle_root(block.transactions.iter());
145
146            assert_match!(
147                consensus.validate_and_insert_block(block.to_immutable()).virtual_state_task.await,
148                Err(RuleError::BadCoinbasePayload(_))
149            );
150        }
151
152        let valid_block_child = consensus.build_block_with_parents_and_transactions(6.into(), vec![3.into()], vec![]);
153        consensus.validate_and_insert_block(valid_block_child.clone().to_immutable()).virtual_state_task.await.unwrap();
154        {
155            // The block DAA score is 2, so the subsidy should be calculated according to the deflationary stage.
156            let mut block = consensus.build_block_with_parents_and_transactions(7.into(), vec![6.into()], vec![]);
157            block.transactions[0].payload[8..16].copy_from_slice(&(5_u64).to_le_bytes());
158            block.header.hash_merkle_root = calc_hash_merkle_root(block.transactions.iter());
159            assert_match!(consensus.validate_and_insert_block(block.to_immutable()).virtual_state_task.await, Err(RuleError::WrongSubsidy(expected,_)) if expected == 44000000000);
160        }
161
162        {
163            // Check that the same daa score as the block's daa score or higher fails, but lower passes.
164            let tip_daa_score = valid_block_child.header.daa_score + 1;
165            check_for_lock_time_and_sequence(&consensus, valid_block_child.header.hash, 8.into(), tip_daa_score + 1, 0, false).await;
166            check_for_lock_time_and_sequence(&consensus, valid_block_child.header.hash, 9.into(), tip_daa_score, 0, false).await;
167            check_for_lock_time_and_sequence(&consensus, valid_block_child.header.hash, 10.into(), tip_daa_score - 1, 0, true).await;
168
169            let valid_block_child_gd = consensus.ghostdag_store().get_data(valid_block_child.header.hash).unwrap();
170            let (valid_block_child_gd_pmt, _) = consensus.window_manager().calc_past_median_time(&valid_block_child_gd).unwrap();
171            let past_median_time = valid_block_child_gd_pmt + 1;
172
173            // Check that the same past median time as the block's or higher fails, but lower passes.
174            let tip_daa_score = valid_block_child.header.daa_score + 1;
175            check_for_lock_time_and_sequence(&consensus, valid_block_child.header.hash, 11.into(), past_median_time + 1, 0, false)
176                .await;
177            check_for_lock_time_and_sequence(&consensus, valid_block_child.header.hash, 12.into(), past_median_time, 0, false).await;
178            check_for_lock_time_and_sequence(&consensus, valid_block_child.header.hash, 13.into(), past_median_time - 1, 0, true)
179                .await;
180
181            // We check that if the transaction is marked as finalized it'll pass for any lock time.
182            check_for_lock_time_and_sequence(
183                &consensus,
184                valid_block_child.header.hash,
185                14.into(),
186                past_median_time + 1,
187                u64::MAX,
188                true,
189            )
190            .await;
191
192            check_for_lock_time_and_sequence(&consensus, valid_block_child.header.hash, 15.into(), tip_daa_score + 1, u64::MAX, true)
193                .await;
194        }
195
196        consensus.shutdown(wait_handles);
197    }
198
199    async fn check_for_lock_time_and_sequence(
200        consensus: &TestConsensus,
201        parent: Hash,
202        block_hash: Hash,
203        lock_time: u64,
204        sequence: u64,
205        should_pass: bool,
206    ) {
207        // The block DAA score is 2, so the subsidy should be calculated according to the deflationary stage.
208        let block = consensus.build_block_with_parents_and_transactions(
209            block_hash,
210            vec![parent],
211            vec![Transaction::new(
212                TX_VERSION,
213                vec![TransactionInput::new(TransactionOutpoint::new(1.into(), 0), vec![], sequence, 0)],
214                vec![],
215                lock_time,
216                SUBNETWORK_ID_NATIVE,
217                0,
218                vec![],
219            )],
220        );
221
222        if should_pass {
223            consensus.validate_and_insert_block(block.to_immutable()).virtual_state_task.await.unwrap();
224        } else {
225            assert_match!(
226                consensus.validate_and_insert_block(block.to_immutable()).virtual_state_task.await,
227                Err(RuleError::TxInContextFailed(_, e)) if matches!(e, TxRuleError::NotFinalized(_)));
228        }
229    }
230}