use crate::{consensus::test_consensus::TestConsensus, model::services::reachability::ReachabilityService};
use kaspa_consensus_core::{
api::ConsensusApi,
block::{Block, BlockTemplate, MutableBlock, TemplateBuildMode, TemplateTransactionSelector},
blockhash,
blockstatus::BlockStatus,
coinbase::MinerData,
config::{params::MAINNET_PARAMS, ConfigBuilder},
tx::{ScriptPublicKey, ScriptVec, Transaction},
BlockHashSet,
};
use kaspa_hashes::Hash;
use std::{collections::VecDeque, thread::JoinHandle};
struct OnetimeTxSelector {
txs: Option<Vec<Transaction>>,
}
impl OnetimeTxSelector {
fn new(txs: Vec<Transaction>) -> Self {
Self { txs: Some(txs) }
}
}
impl TemplateTransactionSelector for OnetimeTxSelector {
fn select_transactions(&mut self) -> Vec<Transaction> {
self.txs.take().unwrap()
}
fn reject_selection(&mut self, _tx_id: kaspa_consensus_core::tx::TransactionId) {
unimplemented!()
}
fn is_successful(&self) -> bool {
true
}
}
struct TestContext {
consensus: TestConsensus,
join_handles: Vec<JoinHandle<()>>,
miner_data: MinerData,
simulated_time: u64,
current_templates: VecDeque<BlockTemplate>,
current_tips: BlockHashSet,
}
impl Drop for TestContext {
fn drop(&mut self) {
self.consensus.shutdown(std::mem::take(&mut self.join_handles));
}
}
impl TestContext {
fn new(consensus: TestConsensus) -> Self {
let join_handles = consensus.init();
let genesis_hash = consensus.params().genesis.hash;
let simulated_time = consensus.params().genesis.timestamp;
Self {
consensus,
join_handles,
miner_data: new_miner_data(),
simulated_time,
current_templates: Default::default(),
current_tips: BlockHashSet::from_iter([genesis_hash]),
}
}
pub fn build_block_template_row(&mut self, nonces: impl Iterator<Item = usize>) -> &mut Self {
for nonce in nonces {
self.simulated_time += self.consensus.params().target_time_per_block;
self.current_templates.push_back(self.build_block_template(nonce as u64, self.simulated_time));
}
self
}
pub fn assert_row_parents(&mut self) -> &mut Self {
for t in self.current_templates.iter() {
assert_eq!(self.current_tips, BlockHashSet::from_iter(t.block.header.direct_parents().iter().copied()));
}
self
}
pub async fn validate_and_insert_row(&mut self) -> &mut Self {
self.current_tips.clear();
while let Some(t) = self.current_templates.pop_front() {
self.current_tips.insert(t.block.header.hash);
self.validate_and_insert_block(t.block.to_immutable()).await;
}
self
}
pub async fn build_and_insert_disqualified_chain(&mut self, mut parents: Vec<Hash>, len: usize) -> Hash {
for _ in 0..len {
self.simulated_time += self.consensus.params().target_time_per_block;
let b = self.build_block_with_parents(parents, 0, self.simulated_time);
parents = vec![b.header.hash];
self.validate_and_insert_block(b.to_immutable()).await;
}
parents[0]
}
pub fn build_block_template(&self, nonce: u64, timestamp: u64) -> BlockTemplate {
let mut t = self
.consensus
.build_block_template(
self.miner_data.clone(),
Box::new(OnetimeTxSelector::new(Default::default())),
TemplateBuildMode::Standard,
)
.unwrap();
t.block.header.timestamp = timestamp;
t.block.header.nonce = nonce;
t.block.header.finalize();
t
}
pub fn build_block_with_parents(&self, parents: Vec<Hash>, nonce: u64, timestamp: u64) -> MutableBlock {
let mut b = self.consensus.build_block_with_parents_and_transactions(blockhash::NONE, parents, Default::default());
b.header.timestamp = timestamp;
b.header.nonce = nonce;
b.header.finalize(); b
}
pub async fn validate_and_insert_block(&mut self, block: Block) -> &mut Self {
let status = self.consensus.validate_and_insert_block(block).virtual_state_task.await.unwrap();
assert!(status.has_block_body());
self
}
pub fn assert_tips(&mut self) -> &mut Self {
assert_eq!(BlockHashSet::from_iter(self.consensus.get_tips().into_iter()), self.current_tips);
self
}
pub fn assert_tips_num(&mut self, expected_num: usize) -> &mut Self {
assert_eq!(BlockHashSet::from_iter(self.consensus.get_tips().into_iter()).len(), expected_num);
self
}
pub fn assert_virtual_parents_subset(&mut self) -> &mut Self {
assert!(self.consensus.get_virtual_parents().is_subset(&self.current_tips));
self
}
pub fn assert_valid_utxo_tip(&mut self) -> &mut Self {
assert!(self.consensus.body_tips().iter().copied().any(|h| self.consensus.block_status(h) == BlockStatus::StatusUTXOValid));
self
}
}
#[tokio::test]
async fn template_mining_sanity_test() {
let config = ConfigBuilder::new(MAINNET_PARAMS).skip_proof_of_work().build();
let mut ctx = TestContext::new(TestConsensus::new(&config));
let rounds = 10;
let width = 3;
for _ in 0..rounds {
ctx.build_block_template_row(0..width)
.assert_row_parents()
.validate_and_insert_row()
.await
.assert_tips()
.assert_virtual_parents_subset()
.assert_valid_utxo_tip();
}
}
#[tokio::test]
async fn antichain_merge_test() {
let config = ConfigBuilder::new(MAINNET_PARAMS)
.skip_proof_of_work()
.edit_consensus_params(|p| {
p.max_block_parents = 4;
p.mergeset_size_limit = 10;
})
.build();
let mut ctx = TestContext::new(TestConsensus::new(&config));
ctx.build_block_template_row(0..32)
.validate_and_insert_row()
.await
.assert_tips()
.assert_virtual_parents_subset()
.assert_valid_utxo_tip();
for _ in 0..32 {
ctx.build_block_template_row(0..1).validate_and_insert_row().await.assert_valid_utxo_tip();
}
ctx.assert_tips_num(1);
}
#[tokio::test]
async fn basic_utxo_disqualified_test() {
kaspa_core::log::try_init_logger("info");
let config = ConfigBuilder::new(MAINNET_PARAMS)
.skip_proof_of_work()
.edit_consensus_params(|p| {
p.max_block_parents = 4;
p.mergeset_size_limit = 10;
})
.build();
let mut ctx = TestContext::new(TestConsensus::new(&config));
for _ in 0..10 {
ctx.build_block_template_row(0..1).validate_and_insert_row().await.assert_valid_utxo_tip();
}
let sink = ctx.consensus.get_sink();
let disqualified_tip = ctx.build_and_insert_disqualified_chain(vec![config.genesis.hash], 20).await;
assert_ne!(sink, disqualified_tip);
assert_eq!(sink, ctx.consensus.get_sink());
assert_eq!(BlockHashSet::from_iter([sink, disqualified_tip]), BlockHashSet::from_iter(ctx.consensus.get_tips().into_iter()));
assert!(!ctx.consensus.get_virtual_parents().contains(&disqualified_tip));
}
#[tokio::test]
async fn double_search_disqualified_test() {
kaspa_core::log::try_init_logger("info");
let config = ConfigBuilder::new(MAINNET_PARAMS)
.skip_proof_of_work()
.edit_consensus_params(|p| {
p.max_block_parents = 4;
p.mergeset_size_limit = 10;
p.min_difficulty_window_len = p.legacy_difficulty_window_size;
})
.build();
let mut ctx = TestContext::new(TestConsensus::new(&config));
ctx.build_block_template_row(0..3)
.validate_and_insert_row()
.await
.assert_tips()
.assert_virtual_parents_subset()
.assert_valid_utxo_tip();
let original_sink = ctx.consensus.get_sink();
let mut virtual_parents = ctx.consensus.get_virtual_parents();
assert!(virtual_parents.remove(&original_sink));
let mut iter = virtual_parents.into_iter();
let root_1 = iter.next().unwrap();
let root_2 = iter.next().unwrap();
assert_eq!(iter.next(), None);
for _ in 0..10 {
ctx.build_block_template_row(0..1).validate_and_insert_row().await.assert_valid_utxo_tip();
}
let sink = ctx.consensus.get_sink();
assert!(ctx.consensus.reachability_service().is_chain_ancestor_of(original_sink, sink));
let disqualified_tip_1 = ctx.build_and_insert_disqualified_chain(vec![root_1], 30).await;
let disqualified_tip_2 = ctx.build_and_insert_disqualified_chain(vec![root_2], 20).await;
assert_eq!(ctx.consensus.get_block_status(root_1), Some(BlockStatus::StatusUTXOValid));
assert_eq!(ctx.consensus.get_block_status(root_2), Some(BlockStatus::StatusUTXOValid));
assert_ne!(sink, disqualified_tip_1);
assert_ne!(sink, disqualified_tip_2);
assert_eq!(sink, ctx.consensus.get_sink());
assert_eq!(
BlockHashSet::from_iter([sink, disqualified_tip_1, disqualified_tip_2]),
BlockHashSet::from_iter(ctx.consensus.get_tips().into_iter())
);
assert!(!ctx.consensus.get_virtual_parents().contains(&disqualified_tip_1));
assert!(!ctx.consensus.get_virtual_parents().contains(&disqualified_tip_2));
for _ in 0..30 {
ctx.build_block_template_row(0..1).validate_and_insert_row().await.assert_valid_utxo_tip();
}
ctx.assert_tips_num(1);
}
fn new_miner_data() -> MinerData {
let secp = secp256k1::Secp256k1::new();
let mut rng = rand::thread_rng();
let (_sk, pk) = secp.generate_keypair(&mut rng);
let script = ScriptVec::from_slice(&pk.serialize());
MinerData::new(ScriptPublicKey::new(0, script), vec![])
}