//! Consensus benchmarks for Synor. //! //! Benchmarks transaction validation, block validation, and UTXO operations. //! Run with: cargo bench -p synor-consensus use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use synor_consensus::{ utxo::{UtxoDiff, UtxoEntry, UtxoSet}, validation::{BlockValidator, TransactionValidator}, }; use synor_types::{ block::{Block, BlockBody, BlockHeader}, transaction::{Outpoint, ScriptPubKey, SubnetworkId, Transaction, TxInput, TxOutput}, Amount, Hash256, Timestamp, }; // ==================== Test Data Helpers ==================== /// Creates a deterministic hash. fn make_hash(n: u64) -> Hash256 { let mut bytes = [0u8; 32]; bytes[..8].copy_from_slice(&n.to_le_bytes()); Hash256::from_bytes(bytes) } /// Creates a test outpoint. fn make_outpoint(tx_n: u64, index: u32) -> Outpoint { Outpoint::new(make_hash(tx_n), index) } /// Creates a P2PKH output. fn make_p2pkh_output(amount: u64) -> TxOutput { TxOutput::new( Amount::from_sompi(amount), ScriptPubKey::p2pkh(&[0u8; 32]), ) } /// Creates a UTXO entry. fn make_utxo_entry(amount: u64, daa_score: u64, is_coinbase: bool) -> UtxoEntry { UtxoEntry::new(make_p2pkh_output(amount), daa_score, is_coinbase) } /// Creates a valid regular transaction with given input/output counts. fn make_transaction(input_count: usize, output_count: usize) -> Transaction { let inputs: Vec = (0..input_count) .map(|i| TxInput::new(make_outpoint(i as u64, 0), vec![0u8; 100])) .collect(); let amount_per_output = 1_000_000_000u64 / output_count as u64; let outputs: Vec = (0..output_count) .map(|_| make_p2pkh_output(amount_per_output)) .collect(); Transaction { version: 1, inputs, outputs, lock_time: 0, subnetwork_id: SubnetworkId::default(), gas: 0, payload: vec![], } } /// Creates a coinbase transaction. fn make_coinbase(reward: u64) -> Transaction { Transaction::coinbase(vec![make_p2pkh_output(reward)], b"benchmark".to_vec()) } /// Creates a populated UTXO set with n entries. fn make_utxo_set(n: usize) -> UtxoSet { let set = UtxoSet::new(); for i in 0..n { let outpoint = make_outpoint(i as u64, 0); let entry = make_utxo_entry(1_000_000_000, 100, false); set.add(outpoint, entry).unwrap(); } set } /// Creates a test block header. fn make_block_header(parent: Hash256) -> BlockHeader { BlockHeader { version: 1, parents: vec![parent], merkle_root: Hash256::ZERO, accepted_id_merkle_root: Hash256::ZERO, utxo_commitment: Hash256::ZERO, timestamp: Timestamp::now(), bits: 0x1d00ffff, nonce: 0, daa_score: 1000, blue_score: 1000.into(), blue_work: Hash256::ZERO, pruning_point: Hash256::ZERO, } } /// Creates a test block with transactions. fn make_block(tx_count: usize) -> Block { let mut transactions = vec![make_coinbase(50_000_000_000)]; for _ in 1..tx_count { transactions.push(make_transaction(1, 2)); } let body = BlockBody { transactions }; let merkle_root = body.merkle_root(); let mut header = make_block_header(Hash256::ZERO); header.merkle_root = merkle_root; Block { header, body } } // ==================== Transaction Validation Benchmarks ==================== fn tx_structure_validation(c: &mut Criterion) { let mut group = c.benchmark_group("tx_structure_validation"); let validator = TransactionValidator::new(); for (inputs, outputs) in [(1, 1), (2, 2), (5, 5), (10, 10)] { let tx = make_transaction(inputs, outputs); let id = format!("{}in_{}out", inputs, outputs); group.throughput(Throughput::Elements(1)); group.bench_with_input(BenchmarkId::new("validate", &id), &tx, |b, tx| { b.iter(|| black_box(validator.validate_structure(tx))) }); } group.finish(); } fn tx_coinbase_validation(c: &mut Criterion) { let validator = TransactionValidator::new(); let coinbase = make_coinbase(50_000_000_000); c.bench_function("tx_coinbase_validation", |b| { b.iter(|| black_box(validator.validate_structure(&coinbase))) }); } fn tx_utxo_validation(c: &mut Criterion) { let mut group = c.benchmark_group("tx_utxo_validation"); let validator = TransactionValidator::new(); for input_count in [1, 2, 5, 10] { // Create UTXO set with the UTXOs we'll spend let utxo_set = UtxoSet::new(); for i in 0..input_count { let outpoint = make_outpoint(i as u64, 0); let entry = make_utxo_entry(1_000_000_000, 100, false); utxo_set.add(outpoint, entry).unwrap(); } let tx = make_transaction(input_count, 2); group.throughput(Throughput::Elements(1)); group.bench_with_input( BenchmarkId::from_parameter(input_count), &(tx, utxo_set), |b, (tx, utxo_set)| { b.iter(|| black_box(validator.validate_against_utxos(tx, utxo_set, 1000))) }, ); } group.finish(); } // ==================== Block Validation Benchmarks ==================== fn block_header_validation(c: &mut Criterion) { let validator = BlockValidator::new(); let header = make_block_header(Hash256::ZERO); c.bench_function("block_header_validation", |b| { b.iter(|| black_box(validator.validate_header(&header))) }); } fn block_pow_validation(c: &mut Criterion) { let validator = BlockValidator::new(); let header = make_block_header(Hash256::ZERO); // Target that the block will pass let target = Hash256::from_bytes([0xff; 32]); c.bench_function("block_pow_validation", |b| { b.iter(|| black_box(validator.validate_pow(&header, target))) }); } fn block_full_validation(c: &mut Criterion) { let mut group = c.benchmark_group("block_full_validation"); let validator = BlockValidator::new(); for tx_count in [1, 5, 10, 50] { let block = make_block(tx_count); // Create UTXO set for the transactions let utxo_set = UtxoSet::new(); for tx in block.body.transactions.iter().skip(1) { for input in &tx.inputs { let entry = make_utxo_entry(1_000_000_000, 100, false); let _ = utxo_set.add(input.previous_output, entry); } } let expected_reward = Amount::from_sompi(50_000_000_000); group.throughput(Throughput::Elements(tx_count as u64)); group.bench_with_input( BenchmarkId::from_parameter(tx_count), &(block, utxo_set, expected_reward), |b, (block, utxo_set, reward)| { b.iter(|| black_box(validator.validate_block(block, utxo_set, *reward))) }, ); } group.finish(); } // ==================== UTXO Set Benchmarks ==================== fn utxo_lookup(c: &mut Criterion) { let mut group = c.benchmark_group("utxo_lookup"); for set_size in [100, 1000, 10000, 100000] { let utxo_set = make_utxo_set(set_size); let target = make_outpoint((set_size / 2) as u64, 0); group.bench_with_input( BenchmarkId::from_parameter(set_size), &(utxo_set, target), |b, (set, target)| { b.iter(|| black_box(set.get(target))) }, ); } group.finish(); } fn utxo_contains(c: &mut Criterion) { let mut group = c.benchmark_group("utxo_contains"); for set_size in [100, 1000, 10000, 100000] { let utxo_set = make_utxo_set(set_size); let target = make_outpoint((set_size / 2) as u64, 0); group.bench_with_input( BenchmarkId::from_parameter(set_size), &(utxo_set, target), |b, (set, target)| { b.iter(|| black_box(set.contains(target))) }, ); } group.finish(); } fn utxo_add(c: &mut Criterion) { let mut group = c.benchmark_group("utxo_add"); for set_size in [100, 1000, 10000] { group.bench_with_input(BenchmarkId::from_parameter(set_size), &set_size, |b, &n| { b.iter_batched( || make_utxo_set(n), |set| { let outpoint = make_outpoint(999999, 0); let entry = make_utxo_entry(1_000_000_000, 100, false); black_box(set.add(outpoint, entry)) }, criterion::BatchSize::SmallInput, ) }); } group.finish(); } fn utxo_remove(c: &mut Criterion) { let mut group = c.benchmark_group("utxo_remove"); for set_size in [100, 1000, 10000] { group.bench_with_input(BenchmarkId::from_parameter(set_size), &set_size, |b, &n| { b.iter_batched( || { let set = make_utxo_set(n); let target = make_outpoint((n / 2) as u64, 0); (set, target) }, |(set, target)| black_box(set.remove(&target)), criterion::BatchSize::SmallInput, ) }); } group.finish(); } // ==================== UTXO Diff Benchmarks ==================== fn utxo_diff_apply(c: &mut Criterion) { let mut group = c.benchmark_group("utxo_diff_apply"); for diff_size in [10, 50, 100, 500] { group.bench_with_input( BenchmarkId::from_parameter(diff_size), &diff_size, |b, &n| { b.iter_batched( || { // Create UTXO set with entries to remove let set = make_utxo_set(n); // Create diff that removes half and adds half let mut diff = UtxoDiff::new(); // Remove first half for i in 0..(n / 2) { diff.remove(make_outpoint(i as u64, 0)); } // Add new entries for i in n..(n + n / 2) { diff.add(make_outpoint(i as u64, 0), make_utxo_entry(500_000_000, 100, false)); } (set, diff) }, |(set, diff)| black_box(set.apply_diff(&diff)), criterion::BatchSize::SmallInput, ) }, ); } group.finish(); } fn utxo_diff_merge(c: &mut Criterion) { let mut group = c.benchmark_group("utxo_diff_merge"); for diff_size in [10, 50, 100] { group.bench_with_input( BenchmarkId::from_parameter(diff_size), &diff_size, |b, &n| { b.iter_batched( || { let mut diff1 = UtxoDiff::new(); let mut diff2 = UtxoDiff::new(); for i in 0..n { diff1.add(make_outpoint(i as u64, 0), make_utxo_entry(100_000_000, 100, false)); } for i in 0..(n / 2) { diff2.remove(make_outpoint(i as u64, 0)); } for i in n..(n * 2) { diff2.add(make_outpoint(i as u64, 0), make_utxo_entry(200_000_000, 100, false)); } (diff1, diff2) }, |(mut diff1, diff2)| { diff1.merge(diff2); black_box(diff1) }, criterion::BatchSize::SmallInput, ) }, ); } group.finish(); } fn utxo_create_tx_diff(c: &mut Criterion) { let mut group = c.benchmark_group("utxo_create_tx_diff"); for input_count in [1, 2, 5, 10] { // Create UTXO set with UTXOs to spend let utxo_set = UtxoSet::new(); for i in 0..input_count { let outpoint = make_outpoint(i as u64, 0); let entry = make_utxo_entry(1_000_000_000, 100, false); utxo_set.add(outpoint, entry).unwrap(); } let tx = make_transaction(input_count, 2); group.throughput(Throughput::Elements(1)); group.bench_with_input( BenchmarkId::from_parameter(input_count), &(utxo_set, tx), |b, (set, tx)| { b.iter(|| black_box(set.create_transaction_diff(tx, 1000))) }, ); } group.finish(); } // ==================== UTXO Selection Benchmarks ==================== fn utxo_get_balance(c: &mut Criterion) { use synor_types::{Address, Network}; // Create a test address from a dummy Ed25519 public key let dummy_pubkey = [0u8; 32]; let address = Address::from_ed25519_pubkey(Network::Mainnet, &dummy_pubkey); c.bench_function("utxo_get_balance_1000", |b| { // For this benchmark, we use an empty set since address matching // requires proper script parsing let utxo_set = make_utxo_set(1000); b.iter(|| black_box(utxo_set.get_balance(&address, Network::Mainnet))) }); } // ==================== Batch Validation Benchmarks ==================== fn batch_tx_validation(c: &mut Criterion) { let mut group = c.benchmark_group("batch_tx_validation"); let validator = TransactionValidator::new(); for batch_size in [10, 50, 100] { let transactions: Vec<_> = (0..batch_size) .map(|_| make_transaction(2, 2)) .collect(); group.throughput(Throughput::Elements(batch_size as u64)); group.bench_with_input( BenchmarkId::from_parameter(batch_size), &transactions, |b, txs| { b.iter(|| { for tx in txs { black_box(validator.validate_structure(tx)).ok(); } }) }, ); } group.finish(); } // ==================== Criterion Groups ==================== criterion_group!( tx_validation_benches, tx_structure_validation, tx_coinbase_validation, tx_utxo_validation, ); criterion_group!( block_validation_benches, block_header_validation, block_pow_validation, block_full_validation, ); criterion_group!( utxo_set_benches, utxo_lookup, utxo_contains, utxo_add, utxo_remove, ); criterion_group!( utxo_diff_benches, utxo_diff_apply, utxo_diff_merge, utxo_create_tx_diff, ); criterion_group!( misc_benches, utxo_get_balance, batch_tx_validation, ); criterion_main!( tx_validation_benches, block_validation_benches, utxo_set_benches, utxo_diff_benches, misc_benches, );