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