Skip to main content

blvm_consensus/block/
mod.rs

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