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        block_validation_context_for_connect_ibd(None::<&[BlockHeader]>, 0, network)
283    }
284}
285
286/// Forwards to [`BlockValidationContext::from_connect_block_ibd_args`] with BIP54 activation override
287/// and boundary timestamps set to `None`.
288#[inline]
289pub fn block_validation_context_for_connect_ibd<H: AsRef<BlockHeader>>(
290    recent_headers: Option<&[H]>,
291    network_time: u64,
292    network: Network,
293) -> BlockValidationContext {
294    BlockValidationContext::from_connect_block_ibd_args(
295        recent_headers,
296        network_time,
297        network,
298        None,
299        None,
300    )
301}
302
303impl IsForkActive for BlockValidationContext {
304    #[inline]
305    fn is_fork_active(&self, fork: crate::types::ForkId, height: u64) -> bool {
306        self.activation.is_fork_active(fork, height)
307    }
308}
309
310#[cfg(feature = "production")]
311mod tx_id_pool {
312    use crate::types::{Hash, Transaction};
313    use std::cell::RefCell;
314
315    thread_local! {
316        static TX_BUF: RefCell<Vec<u8>> = RefCell::new(Vec::with_capacity(
317            crate::optimizations::proven_bounds::MAX_TX_SIZE_PROVEN
318        ));
319    }
320
321    /// Fused serialize+hash using thread-local buffer. Avoids Vec<Vec<u8>> allocation.
322    pub fn compute_tx_id_with_pool(tx: &Transaction) -> Hash {
323        use crate::crypto::OptimizedSha256;
324        use crate::serialization::transaction::serialize_transaction_into;
325
326        TX_BUF.with(|cell| {
327            let mut buf = cell.borrow_mut();
328            serialize_transaction_into(&mut buf, tx);
329            OptimizedSha256::new().hash256(&buf)
330        })
331    }
332}
333
334/// Compute `{ Hash(tx) : tx โˆˆ block.transactions }` for ComputeMerkleRoot (Orange Paper ยง8.4.1).
335///
336/// **Spec reference:** sequential `calculate_tx_id` only (no `rayon`, no `&mut Vec`, no `cfg` in the
337/// body). blvm-spec-lock Z3 translates this for determinism/`ensures`. Optimized paths in
338/// [`compute_block_tx_ids_into`] are tested to match this result.
339#[spec_locked("8.4.1")]
340pub fn compute_block_tx_ids_spec(block: &Block) -> Vec<Hash> {
341    block.transactions.iter().map(calculate_tx_id).collect()
342}
343
344/// Compute transaction IDs for a block (extracted for reuse).
345/// Produces {Hash(tx) : tx โˆˆ block.transactions} for ComputeMerkleRoot input (Orange Paper 8.4.1).
346/// Public so node layer can compute once and share between collect_gaps and connect_block_ibd (#21).
347pub fn compute_block_tx_ids_into(block: &Block, out: &mut Vec<Hash>) {
348    out.clear();
349    out.reserve(block.transactions.len());
350    #[cfg(all(feature = "production", feature = "rayon"))]
351    {
352        use rayon::prelude::*;
353        assert!(
354            block.transactions.len() <= 25_000,
355            "Transaction count {} must be reasonable for batch processing",
356            block.transactions.len()
357        );
358        let chunk: Vec<Hash> = block
359            .transactions
360            .as_ref()
361            .par_iter()
362            .map(tx_id_pool::compute_tx_id_with_pool)
363            .collect();
364        out.extend(chunk);
365    }
366
367    #[cfg(all(feature = "production", not(feature = "rayon")))]
368    {
369        out.extend(
370            block
371                .transactions
372                .iter()
373                .map(tx_id_pool::compute_tx_id_with_pool),
374        );
375    }
376
377    #[cfg(not(feature = "production"))]
378    {
379        out.extend(block.transactions.iter().map(calculate_tx_id));
380    }
381}
382
383pub fn compute_block_tx_ids(block: &Block) -> Vec<Hash> {
384    let mut v = Vec::new();
385    compute_block_tx_ids_into(block, &mut v);
386    v
387}
388
389/// Optimized paths (`rayon` / pooled hash) must agree with [`compute_block_tx_ids_spec`] (ยง8.4.1).
390#[test]
391fn compute_block_tx_ids_spec_matches_optimized_paths() {
392    let coinbase = Transaction {
393        version: 1,
394        inputs: vec![TransactionInput {
395            prevout: OutPoint {
396                hash: [0; 32].into(),
397                index: 0xffffffff,
398            },
399            script_sig: vec![0x03, 0x01, 0x00, 0x00],
400            sequence: 0xffffffff,
401        }]
402        .into(),
403        outputs: vec![TransactionOutput {
404            value: 50_000_000_000,
405            script_pubkey: vec![0x51].into(),
406        }]
407        .into(),
408        lock_time: 0,
409    };
410    let tx2 = Transaction {
411        version: 1,
412        inputs: vec![TransactionInput {
413            prevout: OutPoint {
414                hash: [1u8; 32].into(),
415                index: 0,
416            },
417            script_sig: vec![0x51],
418            sequence: 0xffffffff,
419        }]
420        .into(),
421        outputs: vec![TransactionOutput {
422            value: 10_000,
423            script_pubkey: vec![0x51].into(),
424        }]
425        .into(),
426        lock_time: 0,
427    };
428    let block = Block {
429        header: BlockHeader {
430            version: 1,
431            prev_block_hash: [0; 32],
432            merkle_root: [0; 32],
433            timestamp: 1,
434            bits: 0x207fffff,
435            nonce: 0,
436        },
437        transactions: vec![coinbase, tx2].into_boxed_slice(),
438    };
439    assert_eq!(
440        compute_block_tx_ids_spec(&block),
441        compute_block_tx_ids(&block),
442        "ยง8.4.1 spec reference must match compute_block_tx_ids / compute_block_tx_ids_into"
443    );
444}
445
446#[test]
447fn test_connect_block_invalid_header() {
448    let coinbase_tx = Transaction {
449        version: 1,
450        inputs: vec![TransactionInput {
451            prevout: OutPoint {
452                hash: [0; 32].into(),
453                index: 0xffffffff,
454            },
455            script_sig: vec![],
456            sequence: 0xffffffff,
457        }]
458        .into(),
459        outputs: vec![TransactionOutput {
460            value: 5000000000,
461            script_pubkey: vec![].into(),
462        }]
463        .into(),
464        lock_time: 0,
465    };
466
467    let block = Block {
468        header: BlockHeader {
469            version: 0, // Invalid version
470            prev_block_hash: [0; 32],
471            merkle_root: [0; 32],
472            timestamp: 1231006505,
473            bits: 0x1d00ffff,
474            nonce: 0,
475        },
476        transactions: vec![coinbase_tx].into_boxed_slice(),
477    };
478
479    let utxo_set = UtxoSet::default();
480    let witnesses: Vec<Vec<Witness>> = block
481        .transactions
482        .iter()
483        .map(|tx| {
484            (0..tx.inputs.len())
485                .map(|_| Vec::with_capacity(2))
486                .collect()
487        })
488        .collect();
489    let ctx = BlockValidationContext::for_network(crate::types::Network::Mainnet);
490    let (result, _, _undo_log) = connect_block(&block, &witnesses[..], utxo_set, 0, &ctx).unwrap();
491
492    assert!(matches!(result, ValidationResult::Invalid(_)));
493}
494
495#[test]
496fn test_connect_block_no_transactions() {
497    let block = Block {
498        header: BlockHeader {
499            version: 1,
500            prev_block_hash: [0; 32],
501            merkle_root: [0; 32],
502            timestamp: 1231006505,
503            bits: 0x1d00ffff,
504            nonce: 0,
505        },
506        transactions: vec![].into_boxed_slice(), // No transactions
507    };
508
509    let utxo_set = UtxoSet::default();
510    // One Vec<Witness> per tx (one Witness per input)
511    let witnesses: Vec<Vec<Witness>> = block
512        .transactions
513        .iter()
514        .map(|tx| {
515            (0..tx.inputs.len())
516                .map(|_| Vec::with_capacity(2))
517                .collect()
518        })
519        .collect();
520    let ctx = BlockValidationContext::for_network(crate::types::Network::Mainnet);
521    let (result, _, _undo_log) = connect_block(&block, &witnesses[..], utxo_set, 0, &ctx).unwrap();
522
523    assert!(matches!(result, ValidationResult::Invalid(_)));
524}
525
526#[test]
527fn test_connect_block_first_tx_not_coinbase() {
528    let regular_tx = Transaction {
529        version: 1,
530        inputs: vec![TransactionInput {
531            prevout: OutPoint {
532                hash: [1; 32].into(),
533                index: 0,
534            },
535            script_sig: vec![],
536            sequence: 0xffffffff,
537        }]
538        .into(),
539        outputs: vec![TransactionOutput {
540            value: 1000,
541            script_pubkey: vec![].into(),
542        }]
543        .into(),
544        lock_time: 0,
545    };
546
547    let block = Block {
548        header: BlockHeader {
549            version: 1,
550            prev_block_hash: [0; 32],
551            merkle_root: [0; 32],
552            timestamp: 1231006505,
553            bits: 0x1d00ffff,
554            nonce: 0,
555        },
556        transactions: vec![regular_tx].into_boxed_slice(), // First tx is not coinbase
557    };
558
559    let utxo_set = UtxoSet::default();
560    // One Vec<Witness> per tx (one Witness per input)
561    let witnesses: Vec<Vec<Witness>> = block
562        .transactions
563        .iter()
564        .map(|tx| {
565            (0..tx.inputs.len())
566                .map(|_| Vec::with_capacity(2))
567                .collect()
568        })
569        .collect();
570    let ctx = BlockValidationContext::for_network(crate::types::Network::Mainnet);
571    let (result, _, _undo_log) = connect_block(&block, &witnesses[..], utxo_set, 0, &ctx).unwrap();
572
573    assert!(matches!(result, ValidationResult::Invalid(_)));
574}
575
576#[test]
577fn test_connect_block_coinbase_exceeds_subsidy() {
578    let coinbase_tx = Transaction {
579        version: 1,
580        inputs: vec![TransactionInput {
581            prevout: OutPoint {
582                hash: [0; 32].into(),
583                index: 0xffffffff,
584            },
585            script_sig: vec![],
586            sequence: 0xffffffff,
587        }]
588        .into(),
589        outputs: vec![TransactionOutput {
590            value: 6000000000, // 60 BTC - exceeds subsidy
591            script_pubkey: vec![].into(),
592        }]
593        .into(),
594        lock_time: 0,
595    };
596
597    let block = Block {
598        header: BlockHeader {
599            version: 1,
600            prev_block_hash: [0; 32],
601            merkle_root: [0; 32],
602            timestamp: 1231006505,
603            bits: 0x1d00ffff,
604            nonce: 0,
605        },
606        transactions: vec![coinbase_tx].into_boxed_slice(),
607    };
608
609    let utxo_set = UtxoSet::default();
610    // One Vec<Witness> per tx (one Witness per input)
611    let witnesses: Vec<Vec<Witness>> = block
612        .transactions
613        .iter()
614        .map(|tx| {
615            (0..tx.inputs.len())
616                .map(|_| Vec::with_capacity(2))
617                .collect()
618        })
619        .collect();
620    let ctx = BlockValidationContext::for_network(crate::types::Network::Mainnet);
621    let (result, _, _undo_log) = connect_block(&block, &witnesses[..], utxo_set, 0, &ctx).unwrap();
622
623    assert!(matches!(result, ValidationResult::Invalid(_)));
624}
625
626#[test]
627fn test_apply_transaction_regular() {
628    let mut utxo_set = UtxoSet::default();
629
630    // Add a UTXO first
631    let prev_outpoint = OutPoint {
632        hash: [1; 32],
633        index: 0,
634    };
635    let prev_utxo = UTXO {
636        value: 1000,
637        script_pubkey: vec![OP_1].into(), // OP_1
638        height: 0,
639        is_coinbase: false,
640    };
641    utxo_set.insert(prev_outpoint, std::sync::Arc::new(prev_utxo));
642
643    let regular_tx = Transaction {
644        version: 1,
645        inputs: vec![TransactionInput {
646            prevout: OutPoint {
647                hash: [1; 32].into(),
648                index: 0,
649            },
650            script_sig: vec![OP_1], // OP_1
651            sequence: 0xffffffff,
652        }]
653        .into(),
654        outputs: vec![TransactionOutput {
655            value: 500,
656            script_pubkey: vec![OP_2].into(), // OP_2
657        }]
658        .into(),
659        lock_time: 0,
660    };
661
662    let (new_utxo_set, _undo_entries) = apply_transaction(&regular_tx, utxo_set, 1).unwrap();
663
664    // Should have 1 UTXO (the new output)
665    assert_eq!(new_utxo_set.len(), 1);
666}
667
668#[test]
669fn test_apply_transaction_multiple_outputs() {
670    let coinbase_tx = Transaction {
671        version: 1,
672        inputs: vec![TransactionInput {
673            prevout: OutPoint {
674                hash: [0; 32].into(),
675                index: 0xffffffff,
676            },
677            script_sig: vec![],
678            sequence: 0xffffffff,
679        }]
680        .into(),
681        outputs: vec![
682            TransactionOutput {
683                value: 2500000000,
684                script_pubkey: vec![OP_1].into(),
685            },
686            TransactionOutput {
687                value: 2500000000,
688                script_pubkey: vec![OP_2].into(),
689            },
690        ]
691        .into(),
692        lock_time: 0,
693    };
694
695    let utxo_set = UtxoSet::default();
696    let (new_utxo_set, _undo_entries) = apply_transaction(&coinbase_tx, utxo_set, 0).unwrap();
697
698    assert_eq!(new_utxo_set.len(), 2);
699}
700
701#[test]
702fn test_validate_block_header_valid() {
703    use sha2::{Digest, Sha256};
704
705    // Create a valid header with non-zero merkle root
706    let header = BlockHeader {
707        version: 1,
708        prev_block_hash: [0; 32],
709        merkle_root: Sha256::digest(b"test merkle root")[..].try_into().unwrap(),
710        timestamp: 1231006505,
711        bits: 0x1d00ffff,
712        nonce: 0,
713    };
714
715    let result = header::validate_block_header(&header, None).unwrap();
716    assert!(result);
717}
718
719#[test]
720fn test_validate_block_header_invalid_version() {
721    let header = BlockHeader {
722        version: 0, // Invalid version
723        prev_block_hash: [0; 32],
724        merkle_root: [0; 32],
725        timestamp: 1231006505,
726        bits: 0x1d00ffff,
727        nonce: 0,
728    };
729
730    let result = header::validate_block_header(&header, None).unwrap();
731    assert!(!result);
732}
733
734#[test]
735fn test_validate_block_header_invalid_bits() {
736    let header = BlockHeader {
737        version: 1,
738        prev_block_hash: [0; 32],
739        merkle_root: [0; 32],
740        timestamp: 1231006505,
741        bits: 0, // Invalid bits
742        nonce: 0,
743    };
744
745    let result = header::validate_block_header(&header, None).unwrap();
746    assert!(!result);
747}
748
749#[test]
750fn test_is_coinbase_true() {
751    let coinbase_tx = Transaction {
752        version: 1,
753        inputs: vec![TransactionInput {
754            prevout: OutPoint {
755                hash: [0; 32].into(),
756                index: 0xffffffff,
757            },
758            script_sig: vec![],
759            sequence: 0xffffffff,
760        }]
761        .into(),
762        outputs: vec![TransactionOutput {
763            value: 5000000000,
764            script_pubkey: vec![].into(),
765        }]
766        .into(),
767        lock_time: 0,
768    };
769
770    assert!(is_coinbase(&coinbase_tx));
771}
772
773#[test]
774fn test_is_coinbase_false_wrong_hash() {
775    let regular_tx = Transaction {
776        version: 1,
777        inputs: vec![TransactionInput {
778            prevout: OutPoint {
779                hash: [1; 32].into(),
780                index: 0xffffffff,
781            }, // Wrong hash
782            script_sig: vec![],
783            sequence: 0xffffffff,
784        }]
785        .into(),
786        outputs: vec![TransactionOutput {
787            value: 5000000000,
788            script_pubkey: vec![].into(),
789        }]
790        .into(),
791        lock_time: 0,
792    };
793
794    assert!(!is_coinbase(&regular_tx));
795}
796
797#[test]
798fn test_is_coinbase_false_wrong_index() {
799    let regular_tx = Transaction {
800        version: 1,
801        inputs: vec![TransactionInput {
802            prevout: OutPoint {
803                hash: [0; 32].into(),
804                index: 0,
805            }, // Wrong index
806            script_sig: vec![],
807            sequence: 0xffffffff,
808        }]
809        .into(),
810        outputs: vec![TransactionOutput {
811            value: 5000000000,
812            script_pubkey: vec![].into(),
813        }]
814        .into(),
815        lock_time: 0,
816    };
817
818    assert!(!is_coinbase(&regular_tx));
819}
820
821#[test]
822fn test_is_coinbase_false_multiple_inputs() {
823    let regular_tx = Transaction {
824        version: 1,
825        inputs: vec![
826            TransactionInput {
827                prevout: OutPoint {
828                    hash: [0; 32].into(),
829                    index: 0xffffffff,
830                },
831                script_sig: vec![],
832                sequence: 0xffffffff,
833            },
834            TransactionInput {
835                prevout: OutPoint {
836                    hash: [1; 32],
837                    index: 0,
838                },
839                script_sig: vec![],
840                sequence: 0xffffffff,
841            },
842        ]
843        .into(),
844        outputs: vec![TransactionOutput {
845            value: 5000000000,
846            script_pubkey: vec![].into(),
847        }]
848        .into(),
849        lock_time: 0,
850    };
851
852    assert!(!is_coinbase(&regular_tx));
853}
854
855#[test]
856fn test_calculate_tx_id() {
857    let tx = Transaction {
858        version: 1,
859        inputs: vec![TransactionInput {
860            prevout: OutPoint {
861                hash: [0; 32].into(),
862                index: 0,
863            },
864            script_sig: vec![],
865            sequence: 0xffffffff,
866        }]
867        .into(),
868        outputs: vec![TransactionOutput {
869            value: 1000,
870            script_pubkey: vec![].into(),
871        }]
872        .into(),
873        lock_time: 0,
874    };
875
876    let tx_id = calculate_tx_id(&tx);
877
878    // Should be a 32-byte hash (double SHA256 of serialized transaction)
879    assert_eq!(tx_id.len(), 32);
880
881    // Same transaction should produce same ID (deterministic)
882    let tx_id2 = calculate_tx_id(&tx);
883    assert_eq!(tx_id, tx_id2);
884
885    // Different transaction should produce different ID
886    let mut tx2 = tx.clone();
887    tx2.version = 2;
888    let tx_id3 = calculate_tx_id(&tx2);
889    assert_ne!(tx_id, tx_id3);
890}
891
892#[test]
893fn test_calculate_tx_id_different_versions() {
894    let tx1 = Transaction {
895        version: 2,
896        inputs: vec![].into(),
897        outputs: vec![].into(),
898        lock_time: 0,
899    };
900
901    let tx2 = Transaction {
902        version: 1,
903        inputs: vec![].into(),
904        outputs: vec![].into(),
905        lock_time: 0,
906    };
907
908    let id1 = calculate_tx_id(&tx1);
909    let id2 = calculate_tx_id(&tx2);
910
911    // Different versions should produce different IDs
912    assert_ne!(id1, id2);
913}
914
915#[test]
916fn test_connect_block_empty_transactions() {
917    // Test that blocks with empty transactions are rejected
918    // Note: We need a valid merkle root even for empty blocks (though they're invalid)
919    // For testing purposes, we'll use a zero merkle root which will fail validation
920    let block = Block {
921        header: BlockHeader {
922            version: 1,
923            prev_block_hash: [0; 32],
924            merkle_root: [0; 32], // Zero merkle root - will fail validation
925            timestamp: 1231006505,
926            bits: 0x1d00ffff,
927            nonce: 0,
928        },
929        transactions: vec![].into_boxed_slice(), // Empty transactions - invalid
930    };
931
932    let utxo_set = UtxoSet::default();
933    // Optimization: Pre-allocate witness vectors with capacity
934    let witnesses: Vec<Vec<Witness>> = block
935        .transactions
936        .iter()
937        .map(|tx| tx.inputs.iter().map(|_| Vec::new()).collect())
938        .collect();
939    let ctx = BlockValidationContext::for_network(crate::types::Network::Mainnet);
940    let result = connect_block(&block, &witnesses[..], utxo_set, 0, &ctx);
941    // The result should be Ok with ValidationResult::Invalid
942    assert!(result.is_ok());
943    let (validation_result, _, _undo_log) = result.unwrap();
944    assert!(matches!(validation_result, ValidationResult::Invalid(_)));
945}
946
947#[test]
948fn test_connect_block_invalid_coinbase() {
949    let invalid_coinbase = Transaction {
950        version: 1,
951        inputs: vec![TransactionInput {
952            prevout: OutPoint {
953                hash: [1; 32].into(),
954                index: 0,
955            }, // Wrong hash for coinbase
956            script_sig: vec![],
957            sequence: 0xffffffff,
958        }]
959        .into(),
960        outputs: vec![TransactionOutput {
961            value: 5000000000,
962            script_pubkey: vec![].into(),
963        }]
964        .into(),
965        lock_time: 0,
966    };
967
968    let block = Block {
969        header: BlockHeader {
970            version: 1,
971            prev_block_hash: [0; 32],
972            merkle_root: [0; 32],
973            timestamp: 1231006505,
974            bits: 0x1d00ffff,
975            nonce: 0,
976        },
977        transactions: vec![invalid_coinbase].into_boxed_slice(),
978    };
979
980    let utxo_set = UtxoSet::default();
981    // Optimization: Pre-allocate witness vectors with capacity
982    let witnesses: Vec<Vec<Witness>> = block
983        .transactions
984        .iter()
985        .map(|tx| tx.inputs.iter().map(|_| Vec::new()).collect())
986        .collect();
987    let ctx = BlockValidationContext::for_network(crate::types::Network::Mainnet);
988    let result = connect_block(&block, &witnesses[..], utxo_set, 0, &ctx);
989    // The result should be Ok with ValidationResult::Invalid
990    assert!(result.is_ok());
991    let (validation_result, _, _undo_log) = result.unwrap();
992    assert!(matches!(validation_result, ValidationResult::Invalid(_)));
993}
994
995#[test]
996fn test_apply_transaction_insufficient_funds() {
997    let mut utxo_set = UtxoSet::default();
998
999    // Add a UTXO with insufficient value
1000    let prev_outpoint = OutPoint {
1001        hash: [1; 32],
1002        index: 0,
1003    };
1004    let prev_utxo = UTXO {
1005        value: 100, // Small value
1006        script_pubkey: vec![OP_1].into(),
1007        height: 0,
1008        is_coinbase: false,
1009    };
1010    utxo_set.insert(prev_outpoint, std::sync::Arc::new(prev_utxo));
1011
1012    let tx = Transaction {
1013        version: 1,
1014        inputs: vec![TransactionInput {
1015            prevout: OutPoint {
1016                hash: [1; 32].into(),
1017                index: 0,
1018            },
1019            script_sig: vec![OP_1],
1020            sequence: 0xffffffff,
1021        }]
1022        .into(),
1023        outputs: vec![TransactionOutput {
1024            value: 200, // More than input value
1025            script_pubkey: vec![OP_2].into(),
1026        }]
1027        .into(),
1028        lock_time: 0,
1029    };
1030
1031    // The simplified implementation doesn't validate insufficient funds
1032    let result = apply_transaction(&tx, utxo_set, 1);
1033    assert!(result.is_ok());
1034}
1035
1036#[test]
1037fn test_apply_transaction_missing_utxo() {
1038    let utxo_set = UtxoSet::default(); // Empty UTXO set
1039
1040    let tx = Transaction {
1041        version: 1,
1042        inputs: vec![TransactionInput {
1043            prevout: OutPoint {
1044                hash: [1; 32].into(),
1045                index: 0,
1046            },
1047            script_sig: vec![OP_1],
1048            sequence: 0xffffffff,
1049        }]
1050        .into(),
1051        outputs: vec![TransactionOutput {
1052            value: 100,
1053            script_pubkey: vec![OP_2].into(),
1054        }]
1055        .into(),
1056        lock_time: 0,
1057    };
1058
1059    // The simplified implementation doesn't validate missing UTXOs
1060    let result = apply_transaction(&tx, utxo_set, 1);
1061    assert!(result.is_ok());
1062}
1063
1064#[test]
1065fn test_validate_block_header_future_timestamp() {
1066    use sha2::{Digest, Sha256};
1067
1068    // Create header with non-zero merkle root (required for validation)
1069    // Timestamp validation now uses TimeContext (network time + median time-past)
1070    let header = BlockHeader {
1071        version: 1,
1072        prev_block_hash: [0; 32],
1073        merkle_root: Sha256::digest(b"test merkle root")[..].try_into().unwrap(),
1074        timestamp: 9999999999, // Far future timestamp (would need network time check)
1075        bits: 0x1d00ffff,
1076        nonce: 0,
1077    };
1078
1079    // Header structure is valid (actual future timestamp check needs network context)
1080    let result = header::validate_block_header(&header, None).unwrap();
1081    assert!(result);
1082}
1083
1084#[test]
1085fn test_validate_block_header_zero_timestamp() {
1086    use sha2::{Digest, Sha256};
1087
1088    // Zero timestamp should be rejected by validate_block_header
1089    let header = BlockHeader {
1090        version: 1,
1091        prev_block_hash: [0; 32],
1092        merkle_root: Sha256::digest(b"test merkle root")[..].try_into().unwrap(),
1093        timestamp: 0, // Zero timestamp (invalid)
1094        bits: 0x1d00ffff,
1095        nonce: 0,
1096    };
1097
1098    // Zero timestamp should be rejected
1099    let result = header::validate_block_header(&header, None).unwrap();
1100    assert!(!result);
1101}
1102
1103#[test]
1104fn test_connect_block_coinbase_exceeds_subsidy_edge() {
1105    let coinbase_tx = Transaction {
1106        version: 1,
1107        inputs: vec![TransactionInput {
1108            prevout: OutPoint {
1109                hash: [0; 32].into(),
1110                index: 0xffffffff,
1111            },
1112            script_sig: vec![],
1113            sequence: 0xffffffff,
1114        }]
1115        .into(),
1116        outputs: vec![TransactionOutput {
1117            value: 2100000000000000, // Exceeds total supply
1118            script_pubkey: vec![].into(),
1119        }]
1120        .into(),
1121        lock_time: 0,
1122    };
1123
1124    let block = Block {
1125        header: BlockHeader {
1126            version: 1,
1127            prev_block_hash: [0; 32],
1128            merkle_root: [0; 32],
1129            timestamp: 1231006505,
1130            bits: 0x1d00ffff,
1131            nonce: 0,
1132        },
1133        transactions: vec![coinbase_tx].into_boxed_slice(),
1134    };
1135
1136    let utxo_set = UtxoSet::default();
1137    // Optimization: Pre-allocate witness vectors with capacity
1138    let witnesses: Vec<Vec<Witness>> = block
1139        .transactions
1140        .iter()
1141        .map(|tx| tx.inputs.iter().map(|_| Vec::new()).collect())
1142        .collect();
1143    let ctx = BlockValidationContext::for_network(crate::types::Network::Mainnet);
1144    let result = connect_block(&block, &witnesses[..], utxo_set, 0, &ctx);
1145    // The result should be Ok with ValidationResult::Invalid
1146    assert!(result.is_ok());
1147    let (validation_result, _, _undo_log) = result.unwrap();
1148    assert!(matches!(validation_result, ValidationResult::Invalid(_)));
1149}
1150
1151#[test]
1152fn test_connect_block_first_tx_not_coinbase_edge() {
1153    let regular_tx = Transaction {
1154        version: 1,
1155        inputs: vec![TransactionInput {
1156            prevout: OutPoint {
1157                hash: [1; 32].into(),
1158                index: 0,
1159            },
1160            script_sig: vec![OP_1],
1161            sequence: 0xffffffff,
1162        }]
1163        .into(),
1164        outputs: vec![TransactionOutput {
1165            value: 1000,
1166            script_pubkey: vec![OP_2].into(),
1167        }]
1168        .into(),
1169        lock_time: 0,
1170    };
1171
1172    let block = Block {
1173        header: BlockHeader {
1174            version: 1,
1175            prev_block_hash: [0; 32],
1176            merkle_root: [0; 32],
1177            timestamp: 1231006505,
1178            bits: 0x1d00ffff,
1179            nonce: 0,
1180        },
1181        transactions: vec![regular_tx].into_boxed_slice(), // First tx is not coinbase
1182    };
1183
1184    let utxo_set = UtxoSet::default();
1185    // Optimization: Pre-allocate witness vectors with capacity
1186    let witnesses: Vec<Vec<Witness>> = block
1187        .transactions
1188        .iter()
1189        .map(|tx| tx.inputs.iter().map(|_| Vec::new()).collect())
1190        .collect();
1191    let ctx = BlockValidationContext::for_network(crate::types::Network::Mainnet);
1192    let result = connect_block(&block, &witnesses[..], utxo_set, 0, &ctx);
1193    // The result should be Ok with ValidationResult::Invalid
1194    assert!(result.is_ok());
1195    let (validation_result, _, _undo_log) = result.unwrap();
1196    assert!(matches!(validation_result, ValidationResult::Invalid(_)));
1197}
1198
1199#[test]
1200fn test_apply_transaction_multiple_inputs() {
1201    let mut utxo_set = UtxoSet::default();
1202
1203    // Add multiple UTXOs
1204    let outpoint1 = OutPoint {
1205        hash: [1; 32],
1206        index: 0,
1207    };
1208    let utxo1 = UTXO {
1209        value: 500,
1210        script_pubkey: vec![OP_1].into(),
1211        height: 0,
1212        is_coinbase: false,
1213    };
1214    utxo_set.insert(outpoint1, std::sync::Arc::new(utxo1));
1215
1216    let outpoint2 = OutPoint {
1217        hash: [2; 32],
1218        index: 0,
1219    };
1220    let utxo2 = UTXO {
1221        value: 300,
1222        script_pubkey: vec![OP_2].into(),
1223        height: 0,
1224        is_coinbase: false,
1225    };
1226    utxo_set.insert(outpoint2, std::sync::Arc::new(utxo2));
1227
1228    let tx = Transaction {
1229        version: 1,
1230        inputs: vec![
1231            TransactionInput {
1232                prevout: OutPoint {
1233                    hash: [1; 32].into(),
1234                    index: 0,
1235                },
1236                script_sig: vec![OP_1],
1237                sequence: 0xffffffff,
1238            },
1239            TransactionInput {
1240                prevout: OutPoint {
1241                    hash: [2; 32],
1242                    index: 0,
1243                },
1244                script_sig: vec![OP_2],
1245                sequence: 0xffffffff,
1246            },
1247        ]
1248        .into(),
1249        outputs: vec![TransactionOutput {
1250            value: 700, // Total input value
1251            script_pubkey: vec![OP_3].into(),
1252        }]
1253        .into(),
1254        lock_time: 0,
1255    };
1256
1257    let (new_utxo_set, _undo_entries) = apply_transaction(&tx, utxo_set, 1).unwrap();
1258    assert_eq!(new_utxo_set.len(), 1);
1259}
1260
1261#[test]
1262fn test_apply_transaction_no_outputs() {
1263    let mut utxo_set = UtxoSet::default();
1264
1265    let prev_outpoint = OutPoint {
1266        hash: [1; 32],
1267        index: 0,
1268    };
1269    let prev_utxo = UTXO {
1270        value: 1000,
1271        script_pubkey: vec![OP_1].into(),
1272        height: 0,
1273        is_coinbase: false,
1274    };
1275    utxo_set.insert(prev_outpoint, std::sync::Arc::new(prev_utxo));
1276
1277    // Test that transactions with no outputs are rejected
1278    // This is a validation test, not an application test
1279    let tx = Transaction {
1280        version: 1,
1281        inputs: vec![TransactionInput {
1282            prevout: OutPoint {
1283                hash: [1; 32].into(),
1284                index: 0,
1285            },
1286            script_sig: vec![OP_1],
1287            sequence: 0xffffffff,
1288        }]
1289        .into(),
1290        outputs: vec![].into(), // No outputs - should be invalid
1291        lock_time: 0,
1292    };
1293
1294    // The transaction should be invalid due to no outputs
1295    // We can't apply an invalid transaction, so this test verifies validation rejects it
1296    let validation_result = crate::transaction::check_transaction(&tx).unwrap();
1297    assert!(matches!(validation_result, ValidationResult::Invalid(_)));
1298
1299    // For the actual apply test, use a valid transaction with at least one output
1300    let valid_tx = Transaction {
1301        version: 1,
1302        inputs: vec![TransactionInput {
1303            prevout: OutPoint {
1304                hash: [1; 32].into(),
1305                index: 0,
1306            },
1307            script_sig: vec![OP_1],
1308            sequence: 0xffffffff,
1309        }]
1310        .into(),
1311        outputs: vec![TransactionOutput {
1312            value: 500, // Valid output
1313            script_pubkey: vec![OP_1].into(),
1314        }]
1315        .into(),
1316        lock_time: 0,
1317    };
1318
1319    // Now apply the valid transaction
1320    let (new_utxo_set, _undo_entries) = apply_transaction(&valid_tx, utxo_set, 1).unwrap();
1321    // After applying, the input UTXO should be removed and the output UTXO should be added
1322    assert_eq!(new_utxo_set.len(), 1);
1323
1324    // Verify the output UTXO exists
1325    let output_outpoint = OutPoint {
1326        hash: calculate_tx_id(&valid_tx),
1327        index: 0,
1328    };
1329    assert!(new_utxo_set.contains_key(&output_outpoint));
1330}