Skip to main content

blvm_consensus/block/
mod.rs

1//! Block validation functions from Orange Paper Section 5.3 Section 5.3
2//!
3//! Performance optimizations:
4//! - Parallel transaction validation (production feature)
5//! - Batch UTXO operations
6//! - Assume-Valid Blocks - skip validation for trusted checkpoints
7
8mod apply;
9mod connect;
10mod header;
11mod script_cache;
12pub use apply::{apply_transaction, calculate_tx_id};
13pub(crate) use script_cache::calculate_base_script_flags_for_block;
14#[cfg(not(feature = "production"))]
15pub(crate) use script_cache::calculate_script_flags_for_block_with_base;
16pub use script_cache::{
17    calculate_base_script_flags_for_block_network, calculate_script_flags_for_block_network,
18};
19
20use crate::activation::{ForkActivationTable, IsForkActive};
21use crate::bip113::get_median_time_past;
22use crate::error::Result;
23use crate::segwit::Witness;
24use crate::types::*;
25use blvm_spec_lock::spec_locked;
26#[cfg(feature = "production")]
27use rustc_hash::{FxHashMap, FxHashSet};
28
29#[cfg(test)]
30use crate::constants::*;
31#[cfg(test)]
32use crate::opcodes::*;
33#[cfg(test)]
34use crate::transaction::{check_transaction, is_coinbase};
35
36// Rayon is used conditionally in the code, imported where needed
37
38/// Overlay delta for disk sync. Returned by connect_block_ibd when BLVM_USE_OVERLAY_DELTA=1.
39/// Node converts to SyncBatch and calls apply_sync_batch instead of sync_block_to_batch.
40/// Arc<UTXO> in additions avoids clone in apply_sync_batch hot path.
41///
42/// Single struct definition; production uses faster hashers (FxHashMap/FxHashSet).
43#[derive(Debug, Clone)]
44pub struct UtxoDeltaInner<M, S> {
45    pub additions: M,
46    pub deletions: S,
47}
48
49#[cfg(feature = "production")]
50pub type UtxoDelta = UtxoDeltaInner<
51    FxHashMap<OutPoint, std::sync::Arc<UTXO>>,
52    FxHashSet<crate::utxo_overlay::UtxoDeletionKey>,
53>;
54#[cfg(not(feature = "production"))]
55pub type UtxoDelta = UtxoDeltaInner<
56    std::collections::HashMap<OutPoint, std::sync::Arc<UTXO>>,
57    std::collections::HashSet<crate::utxo_overlay::UtxoDeletionKey>,
58>;
59
60/// Assume-valid checkpoint configuration
61///
62/// Blocks before this height are assumed valid (signature verification skipped)
63/// for faster IBD. This is safe because:
64/// 1. These blocks are in the chain history (already validated by network)
65/// 2. We still validate block structure, Merkle roots, and PoW
66/// 3. Only signature verification is skipped (the expensive operation)
67///
68/// Assume-valid: skip signature verification below configurable height
69/// Default: 0 (validate all blocks) - can be configured via environment or config
70/// Get assume-valid height from configuration
71///
72/// This function loads the assume-valid checkpoint height from environment variable
73/// or configuration. Blocks before this height skip expensive signature verification
74/// during initial block download for performance.
75///
76/// # Configuration
77/// - Environment variable: `BLVM_ASSUME_VALID_HEIGHT` (decimal height)
78/// - Default: 0 (validate all blocks - safest option)
79/// - Benchmarking: `config::set_assume_valid_height()` when `benchmarking` feature enabled
80///
81/// # Safety
82/// This optimization is safe because:
83/// 1. These blocks are already validated by the network
84/// 2. We still validate block structure, Merkle roots, and PoW
85/// 3. Only signature verification is skipped (the expensive operation)
86///
87/// Assume-valid: skip signature verification below configurable height
88#[cfg(feature = "production")]
89#[cfg(all(feature = "production", feature = "rayon"))]
90pub(crate) fn skip_script_exec_cache() -> bool {
91    use std::sync::OnceLock;
92    static CACHED: OnceLock<bool> = OnceLock::new();
93    *CACHED.get_or_init(|| {
94        std::env::var("BLVM_SKIP_SCRIPT_CACHE")
95            .map(|v| v == "1")
96            .unwrap_or(false)
97    })
98}
99
100pub fn get_assume_valid_height() -> u64 {
101    // Check for benchmarking override first
102    #[cfg(feature = "benchmarking")]
103    {
104        use std::sync::atomic::{AtomicU64, Ordering};
105        static OVERRIDE: AtomicU64 = AtomicU64::new(u64::MAX);
106        let override_val = OVERRIDE.load(Ordering::Relaxed);
107        if override_val != u64::MAX {
108            return override_val;
109        }
110    }
111
112    crate::config::get_assume_valid_height()
113}
114
115/// ConnectBlock: โ„ฌ ร— ๐’ฒ* ร— ๐’ฐ๐’ฎ ร— โ„• ร— โ„‹* โ†’ {valid, invalid} ร— ๐’ฐ๐’ฎ
116///
117/// For block b = (h, txs) with witnesses ws, UTXO set us at height height, and recent headers:
118/// 1. Validate block header h
119/// 2. For each transaction tx โˆˆ txs:
120///    - Validate tx structure
121///    - Check inputs against us
122///    - Verify scripts (with witness data if available)
123/// 3. Let fees = ฮฃ_{tx โˆˆ txs} fee(tx)
124/// 4. Let subsidy = GetBlockSubsidy(height)
125/// 5. If coinbase output > fees + subsidy: return (invalid, us)
126/// 6. Apply all transactions to us: us' = ApplyTransactions(txs, us)
127/// 7. Return (valid, us')
128///
129/// # Arguments
130///
131/// * `block` - The block to validate and connect
132/// * `witnesses` - Witness data for each transaction in the block (one Witness per transaction)
133/// * `utxo_set` - Current UTXO set (will be modified)
134/// * `height` - Current block height
135/// * `context` - Block validation context (time, network, fork activation, BIP54 boundary). Build with
136///   `BlockValidationContext::from_connect_block_ibd_args`, `from_time_context_and_network`, or `for_network`.
137#[track_caller]
138/// ConnectBlock: Validate and apply a block to the UTXO set.
139#[spec_locked("5.3")]
140pub fn connect_block(
141    block: &Block,
142    witnesses: &[Vec<Witness>],
143    utxo_set: UtxoSet,
144    height: Natural,
145    context: &BlockValidationContext,
146) -> Result<(
147    ValidationResult,
148    UtxoSet,
149    crate::reorganization::BlockUndoLog,
150)> {
151    #[cfg(all(feature = "production", feature = "rayon"))]
152    let block_arc = Some(std::sync::Arc::new(block.clone()));
153    #[cfg(not(all(feature = "production", feature = "rayon")))]
154    let block_arc = None;
155    let (result, new_utxo_set, _tx_ids, undo_log, _delta) = connect::connect_block_inner(
156        block, witnesses, utxo_set, None, height, context, None, None, block_arc, false, None,
157    )?;
158    Ok((result, new_utxo_set, undo_log))
159}
160
161/// ConnectBlock variant optimized for IBD that returns transaction IDs instead of undo log.
162///
163/// Returns `Vec<Hash>` (transaction IDs) instead of `BlockUndoLog`. Caller provides
164/// `context` (build with `BlockValidationContext::from_connect_block_ibd_args` from
165/// recent_headers, network_time, network, BIP54 override, and boundary).
166///
167/// * `bip30_index` - Optional index for O(1) BIP30 duplicate-coinbase check.
168/// * `precomputed_tx_ids` - Optional pre-computed tx IDs; when `Some`, skips hashing in consensus
169///   and returns those IDs as `Cow::Borrowed` (no per-block `Vec` clone).
170#[spec_locked("5.3")]
171pub fn connect_block_ibd<'a>(
172    block: &Block,
173    witnesses: &[Vec<Witness>],
174    utxo_set: UtxoSet,
175    height: Natural,
176    context: &BlockValidationContext,
177    bip30_index: Option<&mut crate::bip_validation::Bip30Index>,
178    precomputed_tx_ids: Option<&'a [Hash]>,
179    block_arc: Option<std::sync::Arc<Block>>,
180    witnesses_arc: Option<&std::sync::Arc<Vec<Vec<Witness>>>>,
181) -> Result<(
182    ValidationResult,
183    UtxoSet,
184    std::borrow::Cow<'a, [Hash]>,
185    Option<UtxoDelta>,
186)> {
187    let (result, new_utxo_set, tx_ids, _undo_log, utxo_delta) = connect::connect_block_inner(
188        block,
189        witnesses,
190        utxo_set,
191        witnesses_arc,
192        height,
193        context,
194        bip30_index,
195        precomputed_tx_ids,
196        block_arc,
197        true,
198        None,
199    )?;
200    Ok((result, new_utxo_set, tx_ids, utxo_delta))
201}
202
203/// Helper to construct a `TimeContext` from recent headers and network time.
204///
205/// # Consensus Engine Purity
206/// This function does NOT call `SystemTime::now()`. The `network_time` parameter
207/// must be provided by the node layer, ensuring the consensus engine remains pure.
208#[spec_locked("5.5")]
209fn build_time_context<H: AsRef<BlockHeader>>(
210    recent_headers: Option<&[H]>,
211    network_time: u64,
212) -> Option<crate::types::TimeContext> {
213    recent_headers.map(|headers| {
214        let median_time_past = get_median_time_past(headers);
215        crate::types::TimeContext {
216            network_time,
217            median_time_past,
218        }
219    })
220}
221
222/// Block validation context: time, network, fork activation, and optional rule data.
223///
224/// Built by the node from headers, clock, chain params, version-bits, and config.
225/// Consensus only reads; it does not compute activation or read config.
226#[derive(Clone)]
227pub struct BlockValidationContext {
228    /// Time context for BIP113 and future-block checks.
229    pub time_context: Option<crate::types::TimeContext>,
230    /// Network time (Unix timestamp). Used when time_context is None for 2-week skip.
231    pub network_time: u64,
232    /// Network (mainnet / testnet / regtest).
233    pub network: crate::types::Network,
234    /// Precomputed fork activation table.
235    pub activation: ForkActivationTable,
236    /// When BIP54 is active and block is at period boundary, timestamps for timewarp; else None.
237    pub bip54_boundary: Option<crate::types::Bip54BoundaryTimestamps>,
238}
239
240impl BlockValidationContext {
241    /// Build context from the same inputs as `connect_block_ibd` (for migration).
242    pub fn from_connect_block_ibd_args<H: AsRef<BlockHeader>>(
243        recent_headers: Option<&[H]>,
244        network_time: u64,
245        network: crate::types::Network,
246        bip54_activation_override: Option<u64>,
247        bip54_boundary: Option<crate::types::Bip54BoundaryTimestamps>,
248    ) -> Self {
249        let time_context = build_time_context(recent_headers, network_time);
250        let activation = ForkActivationTable::from_network_and_bip54_override(
251            network,
252            bip54_activation_override,
253        );
254        Self {
255            time_context,
256            network_time,
257            network,
258            activation,
259            bip54_boundary,
260        }
261    }
262
263    /// Build context from precomputed time context and network.
264    pub fn from_time_context_and_network(
265        time_context: Option<crate::types::TimeContext>,
266        network: crate::types::Network,
267        bip54_boundary: Option<crate::types::Bip54BoundaryTimestamps>,
268    ) -> Self {
269        let network_time = time_context.as_ref().map(|c| c.network_time).unwrap_or(0);
270        let activation = ForkActivationTable::from_network(network);
271        Self {
272            time_context,
273            network_time,
274            network,
275            activation,
276            bip54_boundary,
277        }
278    }
279
280    /// Build context for a network only (no headers, network_time 0, no BIP54). For tests and simple callers.
281    pub fn for_network(network: crate::types::Network) -> Self {
282        Self::from_connect_block_ibd_args(
283            None::<&[crate::types::BlockHeader]>,
284            0,
285            network,
286            None,
287            None,
288        )
289    }
290}
291
292impl IsForkActive for BlockValidationContext {
293    #[inline]
294    fn is_fork_active(&self, fork: crate::types::ForkId, height: u64) -> bool {
295        self.activation.is_fork_active(fork, height)
296    }
297}
298
299#[cfg(feature = "production")]
300mod tx_id_pool {
301    use crate::types::{Hash, Transaction};
302    use std::cell::RefCell;
303
304    thread_local! {
305        static TX_BUF: RefCell<Vec<u8>> = RefCell::new(Vec::with_capacity(
306            crate::optimizations::proven_bounds::MAX_TX_SIZE_PROVEN
307        ));
308    }
309
310    /// Fused serialize+hash using thread-local buffer. Avoids Vec<Vec<u8>> allocation.
311    pub fn compute_tx_id_with_pool(tx: &Transaction) -> Hash {
312        use crate::crypto::OptimizedSha256;
313        use crate::serialization::transaction::serialize_transaction_into;
314
315        TX_BUF.with(|cell| {
316            let mut buf = cell.borrow_mut();
317            serialize_transaction_into(&mut buf, tx);
318            OptimizedSha256::new().hash256(&buf)
319        })
320    }
321}
322
323/// Compute `{ Hash(tx) : tx โˆˆ block.transactions }` for ComputeMerkleRoot (Orange Paper ยง8.4.1).
324///
325/// **Spec reference:** sequential `calculate_tx_id` only (no `rayon`, no `&mut Vec`, no `cfg` in the
326/// body). blvm-spec-lock Z3 translates this for determinism/`ensures`. Optimized paths in
327/// [`compute_block_tx_ids_into`] are tested to match this result.
328#[spec_locked("8.4.1")]
329pub fn compute_block_tx_ids_spec(block: &Block) -> Vec<Hash> {
330    block.transactions.iter().map(calculate_tx_id).collect()
331}
332
333/// Compute transaction IDs for a block (extracted for reuse).
334/// Produces {Hash(tx) : tx โˆˆ block.transactions} for ComputeMerkleRoot input (Orange Paper 8.4.1).
335/// Public so node layer can compute once and share between collect_gaps and connect_block_ibd (#21).
336pub fn compute_block_tx_ids_into(block: &Block, out: &mut Vec<Hash>) {
337    out.clear();
338    out.reserve(block.transactions.len());
339    #[cfg(all(feature = "production", feature = "rayon"))]
340    {
341        use rayon::prelude::*;
342        assert!(
343            block.transactions.len() <= 25_000,
344            "Transaction count {} must be reasonable for batch processing",
345            block.transactions.len()
346        );
347        let chunk: Vec<Hash> = block
348            .transactions
349            .as_ref()
350            .par_iter()
351            .map(tx_id_pool::compute_tx_id_with_pool)
352            .collect();
353        out.extend(chunk);
354    }
355
356    #[cfg(all(feature = "production", not(feature = "rayon")))]
357    {
358        out.extend(
359            block
360                .transactions
361                .iter()
362                .map(tx_id_pool::compute_tx_id_with_pool),
363        );
364    }
365
366    #[cfg(not(feature = "production"))]
367    {
368        out.extend(block.transactions.iter().map(calculate_tx_id));
369    }
370}
371
372pub fn compute_block_tx_ids(block: &Block) -> Vec<Hash> {
373    let mut v = Vec::new();
374    compute_block_tx_ids_into(block, &mut v);
375    v
376}
377
378/// Optimized paths (`rayon` / pooled hash) must agree with [`compute_block_tx_ids_spec`] (ยง8.4.1).
379#[test]
380fn compute_block_tx_ids_spec_matches_optimized_paths() {
381    let coinbase = Transaction {
382        version: 1,
383        inputs: vec![TransactionInput {
384            prevout: OutPoint {
385                hash: [0; 32].into(),
386                index: 0xffffffff,
387            },
388            script_sig: vec![0x03, 0x01, 0x00, 0x00],
389            sequence: 0xffffffff,
390        }]
391        .into(),
392        outputs: vec![TransactionOutput {
393            value: 50_000_000_000,
394            script_pubkey: vec![0x51].into(),
395        }]
396        .into(),
397        lock_time: 0,
398    };
399    let tx2 = Transaction {
400        version: 1,
401        inputs: vec![TransactionInput {
402            prevout: OutPoint {
403                hash: [1u8; 32].into(),
404                index: 0,
405            },
406            script_sig: vec![0x51],
407            sequence: 0xffffffff,
408        }]
409        .into(),
410        outputs: vec![TransactionOutput {
411            value: 10_000,
412            script_pubkey: vec![0x51].into(),
413        }]
414        .into(),
415        lock_time: 0,
416    };
417    let block = Block {
418        header: BlockHeader {
419            version: 1,
420            prev_block_hash: [0; 32],
421            merkle_root: [0; 32],
422            timestamp: 1,
423            bits: 0x207fffff,
424            nonce: 0,
425        },
426        transactions: vec![coinbase, tx2].into_boxed_slice(),
427    };
428    assert_eq!(
429        compute_block_tx_ids_spec(&block),
430        compute_block_tx_ids(&block),
431        "ยง8.4.1 spec reference must match compute_block_tx_ids / compute_block_tx_ids_into"
432    );
433}
434
435#[test]
436fn test_connect_block_invalid_header() {
437    let coinbase_tx = Transaction {
438        version: 1,
439        inputs: vec![TransactionInput {
440            prevout: OutPoint {
441                hash: [0; 32].into(),
442                index: 0xffffffff,
443            },
444            script_sig: vec![],
445            sequence: 0xffffffff,
446        }]
447        .into(),
448        outputs: vec![TransactionOutput {
449            value: 5000000000,
450            script_pubkey: vec![].into(),
451        }]
452        .into(),
453        lock_time: 0,
454    };
455
456    let block = Block {
457        header: BlockHeader {
458            version: 0, // Invalid version
459            prev_block_hash: [0; 32],
460            merkle_root: [0; 32],
461            timestamp: 1231006505,
462            bits: 0x1d00ffff,
463            nonce: 0,
464        },
465        transactions: vec![coinbase_tx].into_boxed_slice(),
466    };
467
468    let utxo_set = UtxoSet::default();
469    let witnesses: Vec<Vec<Witness>> = block
470        .transactions
471        .iter()
472        .map(|tx| {
473            (0..tx.inputs.len())
474                .map(|_| Vec::with_capacity(2))
475                .collect()
476        })
477        .collect();
478    let ctx = BlockValidationContext::for_network(crate::types::Network::Mainnet);
479    let (result, _, _undo_log) = connect_block(&block, &witnesses[..], utxo_set, 0, &ctx).unwrap();
480
481    assert!(matches!(result, ValidationResult::Invalid(_)));
482}
483
484#[test]
485fn test_connect_block_no_transactions() {
486    let block = Block {
487        header: BlockHeader {
488            version: 1,
489            prev_block_hash: [0; 32],
490            merkle_root: [0; 32],
491            timestamp: 1231006505,
492            bits: 0x1d00ffff,
493            nonce: 0,
494        },
495        transactions: vec![].into_boxed_slice(), // No transactions
496    };
497
498    let utxo_set = UtxoSet::default();
499    // One Vec<Witness> per tx (one Witness per input)
500    let witnesses: Vec<Vec<Witness>> = block
501        .transactions
502        .iter()
503        .map(|tx| {
504            (0..tx.inputs.len())
505                .map(|_| Vec::with_capacity(2))
506                .collect()
507        })
508        .collect();
509    let ctx = BlockValidationContext::for_network(crate::types::Network::Mainnet);
510    let (result, _, _undo_log) = connect_block(&block, &witnesses[..], utxo_set, 0, &ctx).unwrap();
511
512    assert!(matches!(result, ValidationResult::Invalid(_)));
513}
514
515#[test]
516fn test_connect_block_first_tx_not_coinbase() {
517    let regular_tx = Transaction {
518        version: 1,
519        inputs: vec![TransactionInput {
520            prevout: OutPoint {
521                hash: [1; 32].into(),
522                index: 0,
523            },
524            script_sig: vec![],
525            sequence: 0xffffffff,
526        }]
527        .into(),
528        outputs: vec![TransactionOutput {
529            value: 1000,
530            script_pubkey: vec![].into(),
531        }]
532        .into(),
533        lock_time: 0,
534    };
535
536    let block = Block {
537        header: BlockHeader {
538            version: 1,
539            prev_block_hash: [0; 32],
540            merkle_root: [0; 32],
541            timestamp: 1231006505,
542            bits: 0x1d00ffff,
543            nonce: 0,
544        },
545        transactions: vec![regular_tx].into_boxed_slice(), // First tx is not coinbase
546    };
547
548    let utxo_set = UtxoSet::default();
549    // One Vec<Witness> per tx (one Witness per input)
550    let witnesses: Vec<Vec<Witness>> = block
551        .transactions
552        .iter()
553        .map(|tx| {
554            (0..tx.inputs.len())
555                .map(|_| Vec::with_capacity(2))
556                .collect()
557        })
558        .collect();
559    let ctx = BlockValidationContext::for_network(crate::types::Network::Mainnet);
560    let (result, _, _undo_log) = connect_block(&block, &witnesses[..], utxo_set, 0, &ctx).unwrap();
561
562    assert!(matches!(result, ValidationResult::Invalid(_)));
563}
564
565#[test]
566fn test_connect_block_coinbase_exceeds_subsidy() {
567    let coinbase_tx = Transaction {
568        version: 1,
569        inputs: vec![TransactionInput {
570            prevout: OutPoint {
571                hash: [0; 32].into(),
572                index: 0xffffffff,
573            },
574            script_sig: vec![],
575            sequence: 0xffffffff,
576        }]
577        .into(),
578        outputs: vec![TransactionOutput {
579            value: 6000000000, // 60 BTC - exceeds subsidy
580            script_pubkey: vec![].into(),
581        }]
582        .into(),
583        lock_time: 0,
584    };
585
586    let block = Block {
587        header: BlockHeader {
588            version: 1,
589            prev_block_hash: [0; 32],
590            merkle_root: [0; 32],
591            timestamp: 1231006505,
592            bits: 0x1d00ffff,
593            nonce: 0,
594        },
595        transactions: vec![coinbase_tx].into_boxed_slice(),
596    };
597
598    let utxo_set = UtxoSet::default();
599    // One Vec<Witness> per tx (one Witness per input)
600    let witnesses: Vec<Vec<Witness>> = block
601        .transactions
602        .iter()
603        .map(|tx| {
604            (0..tx.inputs.len())
605                .map(|_| Vec::with_capacity(2))
606                .collect()
607        })
608        .collect();
609    let ctx = BlockValidationContext::for_network(crate::types::Network::Mainnet);
610    let (result, _, _undo_log) = connect_block(&block, &witnesses[..], utxo_set, 0, &ctx).unwrap();
611
612    assert!(matches!(result, ValidationResult::Invalid(_)));
613}
614
615#[test]
616fn test_apply_transaction_regular() {
617    let mut utxo_set = UtxoSet::default();
618
619    // Add a UTXO first
620    let prev_outpoint = OutPoint {
621        hash: [1; 32],
622        index: 0,
623    };
624    let prev_utxo = UTXO {
625        value: 1000,
626        script_pubkey: vec![OP_1].into(), // OP_1
627        height: 0,
628        is_coinbase: false,
629    };
630    utxo_set.insert(prev_outpoint, std::sync::Arc::new(prev_utxo));
631
632    let regular_tx = Transaction {
633        version: 1,
634        inputs: vec![TransactionInput {
635            prevout: OutPoint {
636                hash: [1; 32].into(),
637                index: 0,
638            },
639            script_sig: vec![OP_1], // OP_1
640            sequence: 0xffffffff,
641        }]
642        .into(),
643        outputs: vec![TransactionOutput {
644            value: 500,
645            script_pubkey: vec![OP_2].into(), // OP_2
646        }]
647        .into(),
648        lock_time: 0,
649    };
650
651    let (new_utxo_set, _undo_entries) = apply_transaction(&regular_tx, utxo_set, 1).unwrap();
652
653    // Should have 1 UTXO (the new output)
654    assert_eq!(new_utxo_set.len(), 1);
655}
656
657#[test]
658fn test_apply_transaction_multiple_outputs() {
659    let coinbase_tx = Transaction {
660        version: 1,
661        inputs: vec![TransactionInput {
662            prevout: OutPoint {
663                hash: [0; 32].into(),
664                index: 0xffffffff,
665            },
666            script_sig: vec![],
667            sequence: 0xffffffff,
668        }]
669        .into(),
670        outputs: vec![
671            TransactionOutput {
672                value: 2500000000,
673                script_pubkey: vec![OP_1].into(),
674            },
675            TransactionOutput {
676                value: 2500000000,
677                script_pubkey: vec![OP_2].into(),
678            },
679        ]
680        .into(),
681        lock_time: 0,
682    };
683
684    let utxo_set = UtxoSet::default();
685    let (new_utxo_set, _undo_entries) = apply_transaction(&coinbase_tx, utxo_set, 0).unwrap();
686
687    assert_eq!(new_utxo_set.len(), 2);
688}
689
690#[test]
691fn test_validate_block_header_valid() {
692    use sha2::{Digest, Sha256};
693
694    // Create a valid header with non-zero merkle root
695    let header = BlockHeader {
696        version: 1,
697        prev_block_hash: [0; 32],
698        merkle_root: Sha256::digest(b"test merkle root")[..].try_into().unwrap(),
699        timestamp: 1231006505,
700        bits: 0x1d00ffff,
701        nonce: 0,
702    };
703
704    let result = header::validate_block_header(&header, None).unwrap();
705    assert!(result);
706}
707
708#[test]
709fn test_validate_block_header_invalid_version() {
710    let header = BlockHeader {
711        version: 0, // Invalid version
712        prev_block_hash: [0; 32],
713        merkle_root: [0; 32],
714        timestamp: 1231006505,
715        bits: 0x1d00ffff,
716        nonce: 0,
717    };
718
719    let result = header::validate_block_header(&header, None).unwrap();
720    assert!(!result);
721}
722
723#[test]
724fn test_validate_block_header_invalid_bits() {
725    let header = BlockHeader {
726        version: 1,
727        prev_block_hash: [0; 32],
728        merkle_root: [0; 32],
729        timestamp: 1231006505,
730        bits: 0, // Invalid bits
731        nonce: 0,
732    };
733
734    let result = header::validate_block_header(&header, None).unwrap();
735    assert!(!result);
736}
737
738#[test]
739fn test_is_coinbase_true() {
740    let coinbase_tx = Transaction {
741        version: 1,
742        inputs: vec![TransactionInput {
743            prevout: OutPoint {
744                hash: [0; 32].into(),
745                index: 0xffffffff,
746            },
747            script_sig: vec![],
748            sequence: 0xffffffff,
749        }]
750        .into(),
751        outputs: vec![TransactionOutput {
752            value: 5000000000,
753            script_pubkey: vec![].into(),
754        }]
755        .into(),
756        lock_time: 0,
757    };
758
759    assert!(is_coinbase(&coinbase_tx));
760}
761
762#[test]
763fn test_is_coinbase_false_wrong_hash() {
764    let regular_tx = Transaction {
765        version: 1,
766        inputs: vec![TransactionInput {
767            prevout: OutPoint {
768                hash: [1; 32].into(),
769                index: 0xffffffff,
770            }, // Wrong hash
771            script_sig: vec![],
772            sequence: 0xffffffff,
773        }]
774        .into(),
775        outputs: vec![TransactionOutput {
776            value: 5000000000,
777            script_pubkey: vec![].into(),
778        }]
779        .into(),
780        lock_time: 0,
781    };
782
783    assert!(!is_coinbase(&regular_tx));
784}
785
786#[test]
787fn test_is_coinbase_false_wrong_index() {
788    let regular_tx = Transaction {
789        version: 1,
790        inputs: vec![TransactionInput {
791            prevout: OutPoint {
792                hash: [0; 32].into(),
793                index: 0,
794            }, // Wrong index
795            script_sig: vec![],
796            sequence: 0xffffffff,
797        }]
798        .into(),
799        outputs: vec![TransactionOutput {
800            value: 5000000000,
801            script_pubkey: vec![].into(),
802        }]
803        .into(),
804        lock_time: 0,
805    };
806
807    assert!(!is_coinbase(&regular_tx));
808}
809
810#[test]
811fn test_is_coinbase_false_multiple_inputs() {
812    let regular_tx = Transaction {
813        version: 1,
814        inputs: vec![
815            TransactionInput {
816                prevout: OutPoint {
817                    hash: [0; 32].into(),
818                    index: 0xffffffff,
819                },
820                script_sig: vec![],
821                sequence: 0xffffffff,
822            },
823            TransactionInput {
824                prevout: OutPoint {
825                    hash: [1; 32],
826                    index: 0,
827                },
828                script_sig: vec![],
829                sequence: 0xffffffff,
830            },
831        ]
832        .into(),
833        outputs: vec![TransactionOutput {
834            value: 5000000000,
835            script_pubkey: vec![].into(),
836        }]
837        .into(),
838        lock_time: 0,
839    };
840
841    assert!(!is_coinbase(&regular_tx));
842}
843
844#[test]
845fn test_calculate_tx_id() {
846    let tx = Transaction {
847        version: 1,
848        inputs: vec![TransactionInput {
849            prevout: OutPoint {
850                hash: [0; 32].into(),
851                index: 0,
852            },
853            script_sig: vec![],
854            sequence: 0xffffffff,
855        }]
856        .into(),
857        outputs: vec![TransactionOutput {
858            value: 1000,
859            script_pubkey: vec![].into(),
860        }]
861        .into(),
862        lock_time: 0,
863    };
864
865    let tx_id = calculate_tx_id(&tx);
866
867    // Should be a 32-byte hash (double SHA256 of serialized transaction)
868    assert_eq!(tx_id.len(), 32);
869
870    // Same transaction should produce same ID (deterministic)
871    let tx_id2 = calculate_tx_id(&tx);
872    assert_eq!(tx_id, tx_id2);
873
874    // Different transaction should produce different ID
875    let mut tx2 = tx.clone();
876    tx2.version = 2;
877    let tx_id3 = calculate_tx_id(&tx2);
878    assert_ne!(tx_id, tx_id3);
879}
880
881#[test]
882fn test_calculate_tx_id_different_versions() {
883    let tx1 = Transaction {
884        version: 2,
885        inputs: vec![].into(),
886        outputs: vec![].into(),
887        lock_time: 0,
888    };
889
890    let tx2 = Transaction {
891        version: 1,
892        inputs: vec![].into(),
893        outputs: vec![].into(),
894        lock_time: 0,
895    };
896
897    let id1 = calculate_tx_id(&tx1);
898    let id2 = calculate_tx_id(&tx2);
899
900    // Different versions should produce different IDs
901    assert_ne!(id1, id2);
902}
903
904#[test]
905fn test_connect_block_empty_transactions() {
906    // Test that blocks with empty transactions are rejected
907    // Note: We need a valid merkle root even for empty blocks (though they're invalid)
908    // For testing purposes, we'll use a zero merkle root which will fail validation
909    let block = Block {
910        header: BlockHeader {
911            version: 1,
912            prev_block_hash: [0; 32],
913            merkle_root: [0; 32], // Zero merkle root - will fail validation
914            timestamp: 1231006505,
915            bits: 0x1d00ffff,
916            nonce: 0,
917        },
918        transactions: vec![].into_boxed_slice(), // Empty transactions - invalid
919    };
920
921    let utxo_set = UtxoSet::default();
922    // Optimization: Pre-allocate witness vectors with capacity
923    let witnesses: Vec<Vec<Witness>> = block
924        .transactions
925        .iter()
926        .map(|tx| tx.inputs.iter().map(|_| Vec::new()).collect())
927        .collect();
928    let ctx = BlockValidationContext::for_network(crate::types::Network::Mainnet);
929    let result = connect_block(&block, &witnesses[..], utxo_set, 0, &ctx);
930    // The result should be Ok with ValidationResult::Invalid
931    assert!(result.is_ok());
932    let (validation_result, _, _undo_log) = result.unwrap();
933    assert!(matches!(validation_result, ValidationResult::Invalid(_)));
934}
935
936#[test]
937fn test_connect_block_invalid_coinbase() {
938    let invalid_coinbase = Transaction {
939        version: 1,
940        inputs: vec![TransactionInput {
941            prevout: OutPoint {
942                hash: [1; 32].into(),
943                index: 0,
944            }, // Wrong hash for coinbase
945            script_sig: vec![],
946            sequence: 0xffffffff,
947        }]
948        .into(),
949        outputs: vec![TransactionOutput {
950            value: 5000000000,
951            script_pubkey: vec![].into(),
952        }]
953        .into(),
954        lock_time: 0,
955    };
956
957    let block = Block {
958        header: BlockHeader {
959            version: 1,
960            prev_block_hash: [0; 32],
961            merkle_root: [0; 32],
962            timestamp: 1231006505,
963            bits: 0x1d00ffff,
964            nonce: 0,
965        },
966        transactions: vec![invalid_coinbase].into_boxed_slice(),
967    };
968
969    let utxo_set = UtxoSet::default();
970    // Optimization: Pre-allocate witness vectors with capacity
971    let witnesses: Vec<Vec<Witness>> = block
972        .transactions
973        .iter()
974        .map(|tx| tx.inputs.iter().map(|_| Vec::new()).collect())
975        .collect();
976    let ctx = BlockValidationContext::for_network(crate::types::Network::Mainnet);
977    let result = connect_block(&block, &witnesses[..], utxo_set, 0, &ctx);
978    // The result should be Ok with ValidationResult::Invalid
979    assert!(result.is_ok());
980    let (validation_result, _, _undo_log) = result.unwrap();
981    assert!(matches!(validation_result, ValidationResult::Invalid(_)));
982}
983
984#[test]
985fn test_apply_transaction_insufficient_funds() {
986    let mut utxo_set = UtxoSet::default();
987
988    // Add a UTXO with insufficient value
989    let prev_outpoint = OutPoint {
990        hash: [1; 32],
991        index: 0,
992    };
993    let prev_utxo = UTXO {
994        value: 100, // Small value
995        script_pubkey: vec![OP_1].into(),
996        height: 0,
997        is_coinbase: false,
998    };
999    utxo_set.insert(prev_outpoint, std::sync::Arc::new(prev_utxo));
1000
1001    let tx = Transaction {
1002        version: 1,
1003        inputs: vec![TransactionInput {
1004            prevout: OutPoint {
1005                hash: [1; 32].into(),
1006                index: 0,
1007            },
1008            script_sig: vec![OP_1],
1009            sequence: 0xffffffff,
1010        }]
1011        .into(),
1012        outputs: vec![TransactionOutput {
1013            value: 200, // More than input value
1014            script_pubkey: vec![OP_2].into(),
1015        }]
1016        .into(),
1017        lock_time: 0,
1018    };
1019
1020    // The simplified implementation doesn't validate insufficient funds
1021    let result = apply_transaction(&tx, utxo_set, 1);
1022    assert!(result.is_ok());
1023}
1024
1025#[test]
1026fn test_apply_transaction_missing_utxo() {
1027    let utxo_set = UtxoSet::default(); // Empty UTXO set
1028
1029    let tx = Transaction {
1030        version: 1,
1031        inputs: vec![TransactionInput {
1032            prevout: OutPoint {
1033                hash: [1; 32].into(),
1034                index: 0,
1035            },
1036            script_sig: vec![OP_1],
1037            sequence: 0xffffffff,
1038        }]
1039        .into(),
1040        outputs: vec![TransactionOutput {
1041            value: 100,
1042            script_pubkey: vec![OP_2].into(),
1043        }]
1044        .into(),
1045        lock_time: 0,
1046    };
1047
1048    // The simplified implementation doesn't validate missing UTXOs
1049    let result = apply_transaction(&tx, utxo_set, 1);
1050    assert!(result.is_ok());
1051}
1052
1053#[test]
1054fn test_validate_block_header_future_timestamp() {
1055    use sha2::{Digest, Sha256};
1056
1057    // Create header with non-zero merkle root (required for validation)
1058    // Timestamp validation now uses TimeContext (network time + median time-past)
1059    let header = BlockHeader {
1060        version: 1,
1061        prev_block_hash: [0; 32],
1062        merkle_root: Sha256::digest(b"test merkle root")[..].try_into().unwrap(),
1063        timestamp: 9999999999, // Far future timestamp (would need network time check)
1064        bits: 0x1d00ffff,
1065        nonce: 0,
1066    };
1067
1068    // Header structure is valid (actual future timestamp check needs network context)
1069    let result = header::validate_block_header(&header, None).unwrap();
1070    assert!(result);
1071}
1072
1073#[test]
1074fn test_validate_block_header_zero_timestamp() {
1075    use sha2::{Digest, Sha256};
1076
1077    // Zero timestamp should be rejected by validate_block_header
1078    let header = BlockHeader {
1079        version: 1,
1080        prev_block_hash: [0; 32],
1081        merkle_root: Sha256::digest(b"test merkle root")[..].try_into().unwrap(),
1082        timestamp: 0, // Zero timestamp (invalid)
1083        bits: 0x1d00ffff,
1084        nonce: 0,
1085    };
1086
1087    // Zero timestamp should be rejected
1088    let result = header::validate_block_header(&header, None).unwrap();
1089    assert!(!result);
1090}
1091
1092#[test]
1093fn test_connect_block_coinbase_exceeds_subsidy_edge() {
1094    let coinbase_tx = Transaction {
1095        version: 1,
1096        inputs: vec![TransactionInput {
1097            prevout: OutPoint {
1098                hash: [0; 32].into(),
1099                index: 0xffffffff,
1100            },
1101            script_sig: vec![],
1102            sequence: 0xffffffff,
1103        }]
1104        .into(),
1105        outputs: vec![TransactionOutput {
1106            value: 2100000000000000, // Exceeds total supply
1107            script_pubkey: vec![].into(),
1108        }]
1109        .into(),
1110        lock_time: 0,
1111    };
1112
1113    let block = Block {
1114        header: BlockHeader {
1115            version: 1,
1116            prev_block_hash: [0; 32],
1117            merkle_root: [0; 32],
1118            timestamp: 1231006505,
1119            bits: 0x1d00ffff,
1120            nonce: 0,
1121        },
1122        transactions: vec![coinbase_tx].into_boxed_slice(),
1123    };
1124
1125    let utxo_set = UtxoSet::default();
1126    // Optimization: Pre-allocate witness vectors with capacity
1127    let witnesses: Vec<Vec<Witness>> = block
1128        .transactions
1129        .iter()
1130        .map(|tx| tx.inputs.iter().map(|_| Vec::new()).collect())
1131        .collect();
1132    let ctx = BlockValidationContext::for_network(crate::types::Network::Mainnet);
1133    let result = connect_block(&block, &witnesses[..], utxo_set, 0, &ctx);
1134    // The result should be Ok with ValidationResult::Invalid
1135    assert!(result.is_ok());
1136    let (validation_result, _, _undo_log) = result.unwrap();
1137    assert!(matches!(validation_result, ValidationResult::Invalid(_)));
1138}
1139
1140#[test]
1141fn test_connect_block_first_tx_not_coinbase_edge() {
1142    let regular_tx = Transaction {
1143        version: 1,
1144        inputs: vec![TransactionInput {
1145            prevout: OutPoint {
1146                hash: [1; 32].into(),
1147                index: 0,
1148            },
1149            script_sig: vec![OP_1],
1150            sequence: 0xffffffff,
1151        }]
1152        .into(),
1153        outputs: vec![TransactionOutput {
1154            value: 1000,
1155            script_pubkey: vec![OP_2].into(),
1156        }]
1157        .into(),
1158        lock_time: 0,
1159    };
1160
1161    let block = Block {
1162        header: BlockHeader {
1163            version: 1,
1164            prev_block_hash: [0; 32],
1165            merkle_root: [0; 32],
1166            timestamp: 1231006505,
1167            bits: 0x1d00ffff,
1168            nonce: 0,
1169        },
1170        transactions: vec![regular_tx].into_boxed_slice(), // First tx is not coinbase
1171    };
1172
1173    let utxo_set = UtxoSet::default();
1174    // Optimization: Pre-allocate witness vectors with capacity
1175    let witnesses: Vec<Vec<Witness>> = block
1176        .transactions
1177        .iter()
1178        .map(|tx| tx.inputs.iter().map(|_| Vec::new()).collect())
1179        .collect();
1180    let ctx = BlockValidationContext::for_network(crate::types::Network::Mainnet);
1181    let result = connect_block(&block, &witnesses[..], utxo_set, 0, &ctx);
1182    // The result should be Ok with ValidationResult::Invalid
1183    assert!(result.is_ok());
1184    let (validation_result, _, _undo_log) = result.unwrap();
1185    assert!(matches!(validation_result, ValidationResult::Invalid(_)));
1186}
1187
1188#[test]
1189fn test_apply_transaction_multiple_inputs() {
1190    let mut utxo_set = UtxoSet::default();
1191
1192    // Add multiple UTXOs
1193    let outpoint1 = OutPoint {
1194        hash: [1; 32],
1195        index: 0,
1196    };
1197    let utxo1 = UTXO {
1198        value: 500,
1199        script_pubkey: vec![OP_1].into(),
1200        height: 0,
1201        is_coinbase: false,
1202    };
1203    utxo_set.insert(outpoint1, std::sync::Arc::new(utxo1));
1204
1205    let outpoint2 = OutPoint {
1206        hash: [2; 32],
1207        index: 0,
1208    };
1209    let utxo2 = UTXO {
1210        value: 300,
1211        script_pubkey: vec![OP_2].into(),
1212        height: 0,
1213        is_coinbase: false,
1214    };
1215    utxo_set.insert(outpoint2, std::sync::Arc::new(utxo2));
1216
1217    let tx = Transaction {
1218        version: 1,
1219        inputs: vec![
1220            TransactionInput {
1221                prevout: OutPoint {
1222                    hash: [1; 32].into(),
1223                    index: 0,
1224                },
1225                script_sig: vec![OP_1],
1226                sequence: 0xffffffff,
1227            },
1228            TransactionInput {
1229                prevout: OutPoint {
1230                    hash: [2; 32],
1231                    index: 0,
1232                },
1233                script_sig: vec![OP_2],
1234                sequence: 0xffffffff,
1235            },
1236        ]
1237        .into(),
1238        outputs: vec![TransactionOutput {
1239            value: 700, // Total input value
1240            script_pubkey: vec![OP_3].into(),
1241        }]
1242        .into(),
1243        lock_time: 0,
1244    };
1245
1246    let (new_utxo_set, _undo_entries) = apply_transaction(&tx, utxo_set, 1).unwrap();
1247    assert_eq!(new_utxo_set.len(), 1);
1248}
1249
1250#[test]
1251fn test_apply_transaction_no_outputs() {
1252    let mut utxo_set = UtxoSet::default();
1253
1254    let prev_outpoint = OutPoint {
1255        hash: [1; 32],
1256        index: 0,
1257    };
1258    let prev_utxo = UTXO {
1259        value: 1000,
1260        script_pubkey: vec![OP_1].into(),
1261        height: 0,
1262        is_coinbase: false,
1263    };
1264    utxo_set.insert(prev_outpoint, std::sync::Arc::new(prev_utxo));
1265
1266    // Test that transactions with no outputs are rejected
1267    // This is a validation test, not an application test
1268    let tx = Transaction {
1269        version: 1,
1270        inputs: vec![TransactionInput {
1271            prevout: OutPoint {
1272                hash: [1; 32].into(),
1273                index: 0,
1274            },
1275            script_sig: vec![OP_1],
1276            sequence: 0xffffffff,
1277        }]
1278        .into(),
1279        outputs: vec![].into(), // No outputs - should be invalid
1280        lock_time: 0,
1281    };
1282
1283    // The transaction should be invalid due to no outputs
1284    // We can't apply an invalid transaction, so this test verifies validation rejects it
1285    let validation_result = crate::transaction::check_transaction(&tx).unwrap();
1286    assert!(matches!(validation_result, ValidationResult::Invalid(_)));
1287
1288    // For the actual apply test, use a valid transaction with at least one output
1289    let valid_tx = Transaction {
1290        version: 1,
1291        inputs: vec![TransactionInput {
1292            prevout: OutPoint {
1293                hash: [1; 32].into(),
1294                index: 0,
1295            },
1296            script_sig: vec![OP_1],
1297            sequence: 0xffffffff,
1298        }]
1299        .into(),
1300        outputs: vec![TransactionOutput {
1301            value: 500, // Valid output
1302            script_pubkey: vec![OP_1].into(),
1303        }]
1304        .into(),
1305        lock_time: 0,
1306    };
1307
1308    // Now apply the valid transaction
1309    let (new_utxo_set, _undo_entries) = apply_transaction(&valid_tx, utxo_set, 1).unwrap();
1310    // After applying, the input UTXO should be removed and the output UTXO should be added
1311    assert_eq!(new_utxo_set.len(), 1);
1312
1313    // Verify the output UTXO exists
1314    let output_outpoint = OutPoint {
1315        hash: calculate_tx_id(&valid_tx),
1316        index: 0,
1317    };
1318    assert!(new_utxo_set.contains_key(&output_outpoint));
1319}