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