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