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