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