Skip to main content

blvm_consensus/
reorganization.rs

1//! Chain reorganization functions from Orange Paper Section 10.3
2
3use crate::block::connect_block;
4use crate::error::Result;
5use crate::segwit::Witness;
6use crate::types::*;
7use blvm_spec_lock::spec_locked;
8use std::collections::HashMap;
9
10/// Reorganization: When a longer chain is found (simplified API)
11///
12/// Simplified version that creates empty witnesses. For full witness support,
13/// use `reorganize_chain_with_witnesses()`.
14#[spec_locked("11.3")]
15pub fn reorganize_chain(
16    new_chain: &[Block],
17    current_chain: &[Block],
18    current_utxo_set: UtxoSet,
19    current_height: Natural,
20    network: crate::types::Network,
21) -> Result<ReorganizationResult> {
22    // Precondition assertions: Validate function inputs
23    assert!(
24        current_height <= i64::MAX as u64,
25        "Current height {current_height} must fit in i64"
26    );
27    assert!(
28        current_utxo_set.len() <= u32::MAX as usize,
29        "Current UTXO set size {} exceeds maximum",
30        current_utxo_set.len()
31    );
32    assert!(
33        new_chain.len() <= 10_000,
34        "New chain length {} must be reasonable",
35        new_chain.len()
36    );
37    assert!(
38        current_chain.len() <= 10_000,
39        "Current chain length {} must be reasonable",
40        current_chain.len()
41    );
42
43    // Create empty witnesses for all blocks (simplified)
44    // CRITICAL FIX: witnesses is now Vec<Vec<Witness>> per block (one Vec per transaction, each containing one Witness per input)
45    let empty_witnesses: Vec<Vec<Vec<Witness>>> = new_chain
46        .iter()
47        .map(|block| {
48            block
49                .transactions
50                .iter()
51                .map(|tx| tx.inputs.iter().map(|_| Vec::new()).collect())
52                .collect()
53        })
54        .collect();
55    // Invariant assertion: Witness count must match block count
56    assert!(
57        empty_witnesses.len() == new_chain.len(),
58        "Witness count {} must match new chain block count {}",
59        empty_witnesses.len(),
60        new_chain.len()
61    );
62
63    reorganize_chain_with_witnesses(
64        new_chain,
65        &empty_witnesses,
66        None, // No headers for median time-past
67        current_chain,
68        current_utxo_set,
69        current_height,
70        None::<fn(&Block) -> Option<Vec<Witness>>>, // No witness retrieval
71        None::<fn(Natural) -> Option<Vec<BlockHeader>>>, // No header retrieval
72        None::<fn(&Hash) -> Option<BlockUndoLog>>,  // No undo log retrieval
73        None::<fn(&Hash, &BlockUndoLog) -> Result<()>>, // No undo log storage
74        network,
75    )
76}
77
78/// Reorganization: When a longer chain is found (full API with witness support)
79///
80/// For new chain with blocks [b1, b2, ..., bn] and current chain with blocks [c1, c2, ..., cm]:
81/// 1. Find common ancestor between new chain and current chain
82/// 2. Disconnect blocks from current chain back to common ancestor
83/// 3. Connect blocks from new chain from common ancestor forward
84/// 4. Return new UTXO set and reorganization result
85///
86/// # Arguments
87///
88/// * `new_chain` - Blocks from the new (longer) chain
89/// * `new_chain_witnesses` - Witness data for each block in new_chain (one Vec<Witness> per block)
90/// * `new_chain_headers` - Recent headers for median time-past calculation (last 11+ headers, oldest to newest)
91/// * `current_chain` - Blocks from the current chain
92/// * `current_utxo_set` - Current UTXO set
93/// * `current_height` - Current block height
94/// * `get_witnesses_for_block` - Optional callback to retrieve witnesses for a block (for current chain disconnection)
95/// * `get_headers_for_height` - Optional callback to retrieve headers for median time-past (for current chain disconnection)
96/// * `get_undo_log_for_block` - Optional callback to retrieve undo log for a block (for current chain disconnection)
97/// * `store_undo_log_for_block` - Optional callback to store undo log for a block (for new chain connection)
98#[allow(clippy::too_many_arguments)]
99#[spec_locked("11.3")]
100pub fn reorganize_chain_with_witnesses(
101    new_chain: &[Block],
102    new_chain_witnesses: &[Vec<Vec<Witness>>], // CRITICAL FIX: Changed from &[Vec<Witness>] to &[Vec<Vec<Witness>>]
103    // Each block has Vec<Vec<Witness>> (one Vec per transaction, each containing one Witness per input)
104    new_chain_headers: Option<&[BlockHeader]>,
105    current_chain: &[Block],
106    current_utxo_set: UtxoSet,
107    current_height: Natural,
108    _get_witnesses_for_block: Option<impl Fn(&Block) -> Option<Vec<Witness>>>,
109    _get_headers_for_height: Option<impl Fn(Natural) -> Option<Vec<BlockHeader>>>,
110    get_undo_log_for_block: Option<impl Fn(&Hash) -> Option<BlockUndoLog>>,
111    store_undo_log_for_block: Option<impl Fn(&Hash, &BlockUndoLog) -> Result<()>>,
112    network: crate::types::Network,
113) -> Result<ReorganizationResult> {
114    // Precondition assertions: Validate function inputs
115    assert!(
116        current_height <= i64::MAX as u64,
117        "Current height {current_height} must fit in i64"
118    );
119    assert!(
120        current_utxo_set.len() <= u32::MAX as usize,
121        "Current UTXO set size {} exceeds maximum",
122        current_utxo_set.len()
123    );
124    assert!(
125        new_chain.len() <= 10_000,
126        "New chain length {} must be reasonable",
127        new_chain.len()
128    );
129    assert!(
130        current_chain.len() <= 10_000,
131        "Current chain length {} must be reasonable",
132        current_chain.len()
133    );
134    assert!(
135        new_chain_witnesses.len() == new_chain.len(),
136        "New chain witness count {} must match block count {}",
137        new_chain_witnesses.len(),
138        new_chain.len()
139    );
140
141    // 1. Find common ancestor by comparing block hashes
142    let common_ancestor = find_common_ancestor(new_chain, current_chain)?;
143    let common_ancestor_header = common_ancestor.header;
144    let common_ancestor_index = common_ancestor.new_chain_index;
145    let current_ancestor_index = common_ancestor.current_chain_index;
146
147    // Invariant assertion: Common ancestor indices must be valid
148    assert!(
149        common_ancestor_index < new_chain.len(),
150        "Common ancestor index {} must be < new chain length {}",
151        common_ancestor_index,
152        new_chain.len()
153    );
154    assert!(
155        current_ancestor_index < current_chain.len(),
156        "Common ancestor index {} must be < current chain length {}",
157        current_ancestor_index,
158        current_chain.len()
159    );
160
161    // 2. Disconnect blocks from current chain back to common ancestor
162    // We disconnect from (current_ancestor_index + 1) to the tip
163    // Undo logs are retrieved from persistent storage via the callback.
164    // The node layer (blvm-node) should provide a callback that uses BlockStore::get_undo_log()
165    // to retrieve undo logs from the database (redb/sled).
166    let mut utxo_set = current_utxo_set;
167    // Invariant assertion: UTXO set size must be reasonable
168    assert!(
169        utxo_set.len() <= u32::MAX as usize,
170        "UTXO set size {} must not exceed maximum",
171        utxo_set.len()
172    );
173
174    // Disconnect from the block after the common ancestor to the tip
175    let disconnect_start = current_ancestor_index + 1;
176    // Invariant assertion: Disconnect start must be valid
177    assert!(
178        disconnect_start <= current_chain.len(),
179        "Disconnect start {} must be <= current chain length {}",
180        disconnect_start,
181        current_chain.len()
182    );
183
184    let mut disconnected_undo_logs: HashMap<Hash, BlockUndoLog> = HashMap::new();
185    // Invariant assertion: Disconnected undo logs must start empty
186    assert!(
187        disconnected_undo_logs.is_empty(),
188        "Disconnected undo logs must start empty"
189    );
190
191    for i in (disconnect_start..current_chain.len()).rev() {
192        // Bounds checking assertion: Block index must be valid
193        assert!(i < current_chain.len(), "Block index {i} out of bounds");
194        if let Some(block) = current_chain.get(i) {
195            // Invariant assertion: Block must have transactions
196            assert!(
197                !block.transactions.is_empty(),
198                "Block at index {i} must have at least one transaction"
199            );
200
201            let block_hash = calculate_block_hash(&block.header);
202            // Invariant assertion: Block hash must be non-zero
203            assert!(block_hash != [0u8; 32], "Block hash must be non-zero");
204
205            // Retrieve undo log from persistent storage via callback
206            // The callback should use BlockStore::get_undo_log() which reads from the database
207            let undo_log = if let Some(ref get_undo_log) = get_undo_log_for_block {
208                get_undo_log(&block_hash).unwrap_or_else(|| {
209                    // If undo log is not found in database, this is an error condition
210                    // Undo logs should always be stored when blocks are connected
211                    // Log a warning but continue with empty undo log for graceful degradation
212                    BlockUndoLog::new()
213                })
214            } else {
215                // No callback provided - cannot retrieve undo log from storage
216                // This should only happen in testing or when undo logs are not needed
217                BlockUndoLog::new()
218            };
219
220            utxo_set = disconnect_block(block, &undo_log, utxo_set, (i as Natural) + 1)?;
221            disconnected_undo_logs.insert(block_hash, undo_log);
222        }
223    }
224
225    // 3. Connect blocks from new chain from common ancestor forward
226    // We connect from (common_ancestor_index + 1) to the tip of new chain
227    // Calculate the height at the common ancestor.
228    // current_chain[i] is at height: current_height - (current_chain.len() - 1 - i)
229    // So ancestor at current_ancestor_index is at:
230    //   current_height - (current_chain.len() - 1 - current_ancestor_index)
231    let blocks_after_ancestor = (current_chain.len() - 1 - current_ancestor_index) as Natural;
232    let common_ancestor_height = current_height.saturating_sub(blocks_after_ancestor);
233    let mut new_height = common_ancestor_height;
234    let mut connected_blocks = Vec::new();
235    let mut connected_undo_logs: HashMap<Hash, BlockUndoLog> = HashMap::new();
236
237    // Ensure witnesses match blocks
238    if new_chain_witnesses.len() != new_chain.len() {
239        return Err(crate::error::ConsensusError::ConsensusRuleViolation(
240            format!(
241                "Witness count {} does not match block count {}",
242                new_chain_witnesses.len(),
243                new_chain.len()
244            )
245            .into(),
246        ));
247    }
248
249    // Connect blocks starting from the block after the common ancestor
250    for (i, block) in new_chain.iter().enumerate().skip(common_ancestor_index + 1) {
251        new_height += 1;
252        // Get witnesses for this block
253        // CRITICAL FIX: witnesses is now Vec<Vec<Witness>> (one Vec per transaction, each containing one Witness per input)
254        let witnesses = new_chain_witnesses.get(i).cloned().unwrap_or_else(|| {
255            block
256                .transactions
257                .iter()
258                .map(|tx| tx.inputs.iter().map(|_| Vec::new()).collect())
259                .collect()
260        });
261
262        // Get recent headers for median time-past (if available)
263        // For the first block in new chain, use provided headers
264        // For subsequent blocks, we'd need headers from the new chain being built
265        // Simplified: use provided headers if available
266        let recent_headers = new_chain_headers;
267
268        // Network time should be provided by node layer, use block timestamp as fallback for reorganization
269        // In production, the node layer should provide adjusted network time
270        let network_time = block.header.timestamp;
271        let context = crate::block::block_validation_context_for_connect_ibd(
272            recent_headers,
273            network_time,
274            network,
275        );
276        let (validation_result, new_utxo_set, undo_log) =
277            connect_block(block, &witnesses, utxo_set, new_height, &context)?;
278
279        if !matches!(validation_result, ValidationResult::Valid) {
280            return Err(crate::error::ConsensusError::ConsensusRuleViolation(
281                format!("Invalid block at height {new_height} during reorganization").into(),
282            ));
283        }
284
285        // Store undo log for this block (keyed by block hash for future retrieval)
286        let block_hash = calculate_block_hash(&block.header);
287
288        // Persist undo log to database via callback (required for future reorganizations)
289        if let Some(ref store_undo_log) = store_undo_log_for_block {
290            if let Err(e) = store_undo_log(&block_hash, &undo_log) {
291                // Continue without persisting — reorg state is still returned in-memory.
292                // Avoid stderr in default release builds; enable `profile` or use a debug build to see this.
293                #[cfg(any(debug_assertions, feature = "profile"))]
294                eprintln!("Warning: Failed to store undo log for block {block_hash:?}: {e}");
295                #[cfg(not(any(debug_assertions, feature = "profile")))]
296                let _ = e;
297            }
298        }
299
300        // Also store in-memory for the reorganization result
301        connected_undo_logs.insert(block_hash, undo_log);
302
303        utxo_set = new_utxo_set;
304        connected_blocks.push(block.clone());
305    }
306
307    // 4. Return reorganization result
308    Ok(ReorganizationResult {
309        new_utxo_set: utxo_set,
310        new_height,
311        common_ancestor: common_ancestor_header,
312        disconnected_blocks: current_chain[disconnect_start..].to_vec(),
313        connected_blocks,
314        reorganization_depth: current_chain.len() - disconnect_start,
315        connected_block_undo_logs: connected_undo_logs,
316    })
317}
318
319/// Update mempool after chain reorganization
320///
321/// This function should be called after successfully reorganizing the chain
322/// to keep the mempool synchronized with the new blockchain state.
323///
324/// Handles:
325/// 1. Removes transactions from mempool that were included in the new chain blocks
326/// 2. Removes transactions that became invalid (their inputs were spent by new chain)
327/// 3. Optionally re-adds transactions from disconnected blocks that are still valid
328///
329/// # Arguments
330///
331/// * `mempool` - Mutable reference to the mempool
332/// * `reorg_result` - The reorganization result
333/// * `utxo_set` - The updated UTXO set after reorganization
334/// * `get_tx_by_id` - Optional function to look up transactions by ID (needed for full validation)
335///
336/// # Returns
337///
338/// Returns a vector of transaction IDs that were removed from the mempool.
339///
340/// # Example
341///
342/// ```rust
343/// use blvm_consensus::reorganization::{reorganize_chain_with_witnesses, update_mempool_after_reorg};
344/// use blvm_consensus::mempool::Mempool;
345/// use blvm_consensus::segwit::Witness;
346///
347/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
348/// # use blvm_consensus::types::*;
349/// # use blvm_consensus::mempool::Mempool;
350/// # let new_chain = vec![];
351/// # let new_witnesses = vec![];
352/// # let current_chain = vec![];
353/// # let current_utxo_set = UtxoSet::default();
354/// # let current_height = 0;
355/// # let mut mempool = Mempool::new();
356/// // Note: This is a simplified example. In practice, chains must have at least one block
357/// // and share a common ancestor. The result may be an error for empty chains.
358/// let reorg_result = reorganize_chain_with_witnesses(
359///     &new_chain,
360///     &new_witnesses,
361///     None,
362///     &current_chain,
363///     current_utxo_set,
364///     current_height,
365///     None::<fn(&Block) -> Option<Vec<Witness>>>,
366///     None::<fn(Natural) -> Option<Vec<BlockHeader>>>,
367///     None::<fn(&blvm_consensus::types::Hash) -> Option<blvm_consensus::reorganization::BlockUndoLog>>,
368///     None::<fn(&blvm_consensus::types::Hash, &blvm_consensus::reorganization::BlockUndoLog) -> blvm_consensus::error::Result<()>>,
369///     Network::Regtest,
370/// );
371/// if let Ok(reorg_result) = reorg_result {
372///     let _removed = update_mempool_after_reorg(
373///         &mut mempool,
374///         &reorg_result,
375///         &reorg_result.new_utxo_set,
376///         None::<fn(&Hash) -> Option<Transaction>>, // No transaction lookup available
377///     )?;
378/// }
379/// # Ok(())
380/// # }
381/// ```
382#[spec_locked("11.3")]
383pub fn update_mempool_after_reorg<F>(
384    mempool: &mut crate::mempool::Mempool,
385    reorg_result: &ReorganizationResult,
386    utxo_set: &UtxoSet,
387    get_tx_by_id: Option<F>,
388) -> Result<Vec<Hash>>
389where
390    F: Fn(&Hash) -> Option<Transaction>,
391{
392    use crate::mempool::update_mempool_after_block;
393
394    let mut all_removed = Vec::new();
395
396    // 1. Remove transactions that were included in the new connected blocks
397    for block in &reorg_result.connected_blocks {
398        let removed = update_mempool_after_block(mempool, block, utxo_set)?;
399        all_removed.extend(removed);
400    }
401
402    // 2. Remove transactions that became invalid (their inputs were spent by new chain)
403    // Collect all spent outpoints from the new connected blocks
404    let mut spent_outpoints = std::collections::HashSet::new();
405    for block in &reorg_result.connected_blocks {
406        for tx in &block.transactions {
407            if !crate::transaction::is_coinbase(tx) {
408                for input in &tx.inputs {
409                    spent_outpoints.insert(input.prevout);
410                }
411            }
412        }
413    }
414
415    // If we have transaction lookup, check each mempool transaction
416    if let Some(lookup) = get_tx_by_id {
417        let mut invalid_tx_ids = Vec::new();
418        for &tx_id in mempool.iter() {
419            if let Some(tx) = lookup(&tx_id) {
420                // Check if any input of this transaction was spent by the new chain
421                for input in &tx.inputs {
422                    if spent_outpoints.contains(&input.prevout) {
423                        invalid_tx_ids.push(tx_id);
424                        break;
425                    }
426                }
427            }
428        }
429
430        // Remove invalid transactions
431        for tx_id in invalid_tx_ids {
432            if mempool.remove(&tx_id) {
433                all_removed.push(tx_id);
434            }
435        }
436    }
437
438    // 3. Optionally re-add transactions from disconnected blocks that are still valid
439    // Note: This is a simplified version. In a full implementation, we'd need to:
440    // - Re-validate transactions against the new UTXO set
441    // - Check if they're still valid (not double-spent, scripts still valid, etc.)
442    // - Re-add them to mempool if valid
443    // For now, we skip this step as it requires full transaction re-validation
444
445    Ok(all_removed)
446}
447
448/// Simplified version without transaction lookup
449#[spec_locked("11.3")]
450pub fn update_mempool_after_reorg_simple(
451    mempool: &mut crate::mempool::Mempool,
452    reorg_result: &ReorganizationResult,
453    utxo_set: &UtxoSet,
454) -> Result<Vec<Hash>> {
455    update_mempool_after_reorg(
456        mempool,
457        reorg_result,
458        utxo_set,
459        None::<fn(&Hash) -> Option<Transaction>>,
460    )
461}
462
463/// Common ancestor result with indices in both chains
464struct CommonAncestorResult {
465    header: BlockHeader,
466    new_chain_index: usize,
467    current_chain_index: usize,
468}
469
470/// Find common ancestor between two chains by comparing block hashes
471///
472/// Algorithm: Start from the tips of both chains and work backwards,
473/// comparing blocks at the same distance from tip until we find a match.
474/// This is the common ancestor where the chains diverged.
475/// Orange Paper 11.3: Chain reorganization finds common ancestor before disconnect/connect.
476#[spec_locked("11.3")]
477fn find_common_ancestor(
478    new_chain: &[Block],
479    current_chain: &[Block],
480) -> Result<CommonAncestorResult> {
481    if new_chain.is_empty() || current_chain.is_empty() {
482        return Err(crate::error::ConsensusError::ConsensusRuleViolation(
483            "Cannot find common ancestor: empty chain".into(),
484        ));
485    }
486
487    // Find the minimum chain length - we can only compare up to this point
488    let min_len = new_chain.len().min(current_chain.len());
489
490    // Work backwards from tips, comparing blocks at the same distance from tip
491    // Distance 0 = tip, distance 1 = one before tip, etc.
492    for distance_from_tip in 0..min_len {
493        let new_idx = new_chain.len() - 1 - distance_from_tip;
494        let current_idx = current_chain.len() - 1 - distance_from_tip;
495
496        let new_hash = calculate_block_hash(&new_chain[new_idx].header);
497        let current_hash = calculate_block_hash(&current_chain[current_idx].header);
498
499        // If hashes match, we found the common ancestor
500        if new_hash == current_hash {
501            return Ok(CommonAncestorResult {
502                header: new_chain[new_idx].header.clone(),
503                new_chain_index: new_idx,
504                current_chain_index: current_idx,
505            });
506        }
507    }
508
509    // If we've checked all blocks up to min_len and none match,
510    // check if genesis blocks match (they should always be the same)
511    if !new_chain.is_empty() && !current_chain.is_empty() {
512        let new_genesis_hash = calculate_block_hash(&new_chain[0].header);
513        let current_genesis_hash = calculate_block_hash(&current_chain[0].header);
514        if new_genesis_hash == current_genesis_hash {
515            return Ok(CommonAncestorResult {
516                header: new_chain[0].header.clone(),
517                new_chain_index: 0,
518                current_chain_index: 0,
519            });
520        }
521    }
522
523    // Chains don't share a common ancestor (should never happen in Bitcoin)
524    Err(crate::error::ConsensusError::ConsensusRuleViolation(
525        "Chains do not share a common ancestor".into(),
526    ))
527}
528
529/// Disconnect a block from the chain (reverse of ConnectBlock)
530///
531/// Uses the undo log to perfectly restore the UTXO set to its state before the block was connected.
532/// This is the inverse operation of `connect_block`.
533/// Orange Paper 11.3.1: DisconnectBlock applies undo log to reverse ConnectBlock.
534///
535/// # Arguments
536///
537/// * `block` - The block to disconnect (used for validation, undo_log contains the actual changes)
538/// * `undo_log` - The undo log created when this block was connected
539/// * `utxo_set` - Current UTXO set (will be modified)
540/// * `_height` - Block height (for potential future use)
541#[spec_locked("11.3.1")]
542fn disconnect_block(
543    _block: &Block,
544    undo_log: &BlockUndoLog,
545    mut utxo_set: UtxoSet,
546    _height: Natural,
547) -> Result<UtxoSet> {
548    // Precondition assertions: Validate function inputs
549    assert!(
550        !_block.transactions.is_empty(),
551        "Block must have at least one transaction"
552    );
553    assert!(
554        _height <= i64::MAX as u64,
555        "Block height {_height} must fit in i64"
556    );
557    assert!(
558        utxo_set.len() <= u32::MAX as usize,
559        "UTXO set size {} must not exceed maximum",
560        utxo_set.len()
561    );
562    // Invariant assertion: Undo log entry count must be reasonable
563    assert!(
564        undo_log.entries.len() <= 10_000,
565        "Undo log entry count {} must be reasonable",
566        undo_log.entries.len()
567    );
568
569    // Process undo entries in reverse order (most recent first)
570    // This reverses the order of operations from connect_block
571    for (i, entry) in undo_log.entries.iter().enumerate() {
572        // Bounds checking assertion: Entry index must be valid
573        assert!(i < undo_log.entries.len(), "Entry index {i} out of bounds");
574        // Remove new UTXO (if it was created by this block)
575        if entry.new_utxo.is_some() {
576            utxo_set.remove(&entry.outpoint);
577        }
578
579        // Restore previous UTXO (if it was spent by this block)
580        if let Some(previous_utxo) = &entry.previous_utxo {
581            utxo_set.insert(entry.outpoint, std::sync::Arc::clone(previous_utxo));
582        }
583    }
584
585    Ok(utxo_set)
586}
587
588/// Check if reorganization is beneficial
589#[track_caller] // Better error messages showing caller location
590#[allow(clippy::redundant_comparisons)] // Intentional assertions for formal verification
591#[spec_locked("11.3")]
592pub fn should_reorganize(new_chain: &[Block], current_chain: &[Block]) -> Result<bool> {
593    // Precondition assertions: Validate function inputs
594    assert!(
595        new_chain.len() <= 10_000,
596        "New chain length {} must be reasonable",
597        new_chain.len()
598    );
599    assert!(
600        current_chain.len() <= 10_000,
601        "Current chain length {} must be reasonable",
602        current_chain.len()
603    );
604
605    // Reorganize if new chain is longer
606    if new_chain.len() > current_chain.len() {
607        // Postcondition assertion: Result must be boolean
608        #[allow(clippy::eq_op)]
609        {
610            assert!(true == true || false == false, "Result must be boolean");
611        }
612        return Ok(true);
613    }
614
615    // Reorganize if chains are same length but new chain has more work
616    if new_chain.len() == current_chain.len() {
617        let new_work = calculate_chain_work(new_chain)?;
618        let current_work = calculate_chain_work(current_chain)?;
619        let result = new_work > current_work;
620        // Postcondition assertion: Result must be boolean
621        // Note: Result is boolean (tautology for formal verification)
622        return Ok(result);
623    }
624
625    // Postcondition assertion: Result must be boolean
626    let result = false;
627    // Note: Result is boolean (tautology for formal verification)
628    Ok(result)
629}
630
631/// Calculate total work for a chain
632/// Orange Paper 11.3: BestChain = argmax Σ Work(block)
633///
634/// Mathematical invariants:
635/// - Work is always non-negative
636/// - Work increases monotonically with chain length
637/// - Work calculation is deterministic
638#[spec_locked("11.3")]
639fn calculate_chain_work(chain: &[Block]) -> Result<u128> {
640    let mut total_work = 0u128;
641
642    for block in chain {
643        let target = expand_target(block.header.bits)?;
644        // Work is proportional to 1/target
645        // Avoid overflow by using checked arithmetic
646        if target > 0 {
647            // Calculate work contribution safely
648            // Work = 2^256 / (target + 1) for Bitcoin
649            // For simplicity, use: work = u128::MAX / (target + 1)
650            // Prevent division by zero and overflow
651            let work_contribution = if target == u128::MAX {
652                0 // Very large target means very small work
653            } else {
654                // Use checked_div to avoid panic, fallback to 0 on overflow
655                u128::MAX.checked_div(target + 1).unwrap_or(0)
656            };
657
658            // u128 is always non-negative - no assertion needed
659
660            let old_total = total_work;
661            total_work = total_work.saturating_add(work_contribution);
662
663            // Runtime assertion: Total work must be non-decreasing
664            debug_assert!(
665                total_work >= old_total,
666                "Total work ({total_work}) must be >= previous total ({old_total})"
667            );
668        }
669        // Zero target means infinite difficulty - skip this block (work = 0)
670    }
671
672    // u128 is always non-negative - no assertion needed
673
674    Ok(total_work)
675}
676
677/// Expand target from compact format (reused from mining module)
678fn expand_target(bits: Natural) -> Result<u128> {
679    let exponent = (bits >> 24) as u8;
680    let mantissa = bits & 0x00ffffff;
681
682    if exponent <= 3 {
683        let shift = 8 * (3 - exponent);
684        Ok((mantissa as u128) >> shift)
685    } else {
686        // Prevent overflow by checking exponent before calculating shift
687        // Maximum safe exponent: 3 + (128 / 8) = 19
688        if exponent > 19 {
689            return Err(crate::error::ConsensusError::InvalidProofOfWork(
690                "Target too large".into(),
691            ));
692        }
693        // Calculate shift safely - exponent is bounded, so no overflow
694        let shift = 8 * (exponent - 3);
695        // Use checked shift to avoid overflow
696        let mantissa_u128 = mantissa as u128;
697        let expanded = mantissa_u128.checked_shl(shift as u32).ok_or_else(|| {
698            crate::error::ConsensusError::InvalidProofOfWork("Target expansion overflow".into())
699        })?;
700        Ok(expanded)
701    }
702}
703
704/// Calculate transaction ID (simplified)
705#[allow(dead_code)] // Used in tests
706fn calculate_tx_id(tx: &Transaction) -> Hash {
707    let mut hash = [0u8; 32];
708    hash[0] = (tx.version & 0xff) as u8;
709    hash[1] = (tx.inputs.len() & 0xff) as u8;
710    hash[2] = (tx.outputs.len() & 0xff) as u8;
711    hash[3] = (tx.lock_time & 0xff) as u8;
712    hash
713}
714
715/// Calculate block hash for indexing undo logs
716///
717/// Uses the block header to compute a unique identifier for the block.
718/// This is used to store and retrieve undo logs during reorganization.
719fn calculate_block_hash(header: &BlockHeader) -> Hash {
720    use sha2::{Digest, Sha256};
721
722    // Serialize block header (80 bytes: version, prev_block_hash, merkle_root, timestamp, bits, nonce)
723    let mut bytes = Vec::with_capacity(80);
724    bytes.extend_from_slice(&header.version.to_le_bytes());
725    bytes.extend_from_slice(&header.prev_block_hash);
726    bytes.extend_from_slice(&header.merkle_root);
727    bytes.extend_from_slice(&header.timestamp.to_le_bytes());
728    bytes.extend_from_slice(&header.bits.to_le_bytes());
729    bytes.extend_from_slice(&header.nonce.to_le_bytes());
730
731    // Double SHA256 (Bitcoin standard)
732    let first_hash = Sha256::digest(&bytes);
733    let second_hash = Sha256::digest(first_hash);
734
735    let mut hash = [0u8; 32];
736    hash.copy_from_slice(&second_hash);
737    hash
738}
739
740// ============================================================================
741// TYPES
742// ============================================================================
743
744mod undo_entry_serde {
745    use crate::types::UTXO;
746    use serde::{Deserialize, Deserializer, Serialize, Serializer};
747    use std::sync::Arc;
748
749    pub fn serialize<S>(opt: &Option<Arc<UTXO>>, s: S) -> Result<S::Ok, S::Error>
750    where
751        S: Serializer,
752    {
753        opt.as_ref().map(|a| a.as_ref()).serialize(s)
754    }
755
756    pub fn deserialize<'de, D>(d: D) -> Result<Option<Arc<UTXO>>, D::Error>
757    where
758        D: Deserializer<'de>,
759    {
760        Option::<UTXO>::deserialize(d).map(|opt| opt.map(Arc::new))
761    }
762}
763
764/// Undo log entry for a single UTXO change
765///
766/// Records the state of a UTXO before and after a transaction is applied.
767/// This allows perfect reversal of UTXO set changes during block disconnection.
768#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
769pub struct UndoEntry {
770    /// The outpoint that was changed
771    pub outpoint: OutPoint,
772    /// The UTXO that existed before (None if it was created by this transaction)
773    #[serde(with = "undo_entry_serde")]
774    pub previous_utxo: Option<std::sync::Arc<UTXO>>,
775    /// The UTXO that exists after (None if it was spent by this transaction)
776    #[serde(with = "undo_entry_serde")]
777    pub new_utxo: Option<std::sync::Arc<UTXO>>,
778}
779
780/// Undo log for a single block
781///
782/// Contains all UTXO changes made by a block, allowing perfect reversal
783/// of the block's effects on the UTXO set.
784///
785/// Entries are stored in reverse order (most recent first) to allow
786/// efficient undo by iterating forward.
787#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
788pub struct BlockUndoLog {
789    /// Entries in reverse order (most recent first)
790    /// This allows efficient undo: iterate forward and restore previous_utxo, remove new_utxo
791    pub entries: Vec<UndoEntry>,
792}
793
794impl BlockUndoLog {
795    /// Create an empty undo log
796    pub fn new() -> Self {
797        Self {
798            entries: Vec::new(),
799        }
800    }
801
802    /// Add an undo entry to the log
803    pub fn push(&mut self, entry: UndoEntry) {
804        self.entries.push(entry);
805    }
806
807    /// Check if the undo log is empty
808    pub fn is_empty(&self) -> bool {
809        self.entries.is_empty()
810    }
811}
812
813impl Default for BlockUndoLog {
814    fn default() -> Self {
815        Self::new()
816    }
817}
818
819/// Result of chain reorganization
820#[derive(Debug, Clone)]
821pub struct ReorganizationResult {
822    pub new_utxo_set: UtxoSet,
823    pub new_height: Natural,
824    pub common_ancestor: BlockHeader,
825    pub disconnected_blocks: Vec<Block>,
826    pub connected_blocks: Vec<Block>,
827    pub reorganization_depth: usize,
828    /// Undo logs for connected blocks (keyed by block hash)
829    /// These can be used for future disconnections
830    pub connected_block_undo_logs: HashMap<Hash, BlockUndoLog>,
831}
832
833// ============================================================================
834// FORMAL VERIFICATION
835// ============================================================================
836
837/// Mathematical Specification for Chain Selection:
838/// ∀ chains C₁, C₂: work(C₁) > work(C₂) ⇒ select(C₁)
839///
840/// Invariants:
841/// - Selected chain has maximum cumulative work
842/// - Work calculation is deterministic
843/// - Empty chains are rejected
844/// - Chain work is always non-negative
845
846#[cfg(test)]
847mod property_tests {
848    use super::*;
849    use proptest::prelude::*;
850
851    /// Helper to get chain length range based on coverage mode
852    fn chain_len_range() -> std::ops::Range<usize> {
853        if std::env::var("CARGO_TARPAULIN").is_ok() || std::env::var("TARPAULIN").is_ok() {
854            1..3 // Reduced range under coverage
855        } else {
856            1..5
857        }
858    }
859
860    /// Helper to get chain length range for deterministic test
861    fn chain_len_range_det() -> std::ops::Range<usize> {
862        if std::env::var("CARGO_TARPAULIN").is_ok() || std::env::var("TARPAULIN").is_ok() {
863            0..3 // Reduced range under coverage
864        } else {
865            0..10
866        }
867    }
868
869    /// Random compact-encoded header bits (same domain as `expand_target` tests).
870    fn arb_block() -> impl Strategy<Value = Block> {
871        (0x00000000u64..0x1d00ffffu64).prop_map(|bits| Block {
872            header: BlockHeader {
873                version: 1,
874                prev_block_hash: [0u8; 32],
875                merkle_root: [0u8; 32],
876                timestamp: 0,
877                bits,
878                nonce: 0,
879            },
880            transactions: Box::new([]),
881        })
882    }
883
884    /// Property test: should_reorganize selects chain with maximum work
885    proptest! {
886        #[test]
887        fn prop_should_reorganize_max_work(
888            new_chain in proptest::collection::vec(arb_block(), chain_len_range()),
889            current_chain in proptest::collection::vec(arb_block(), chain_len_range())
890        ) {
891            // Calculate work for both chains - handle errors from invalid blocks
892            let new_work = calculate_chain_work(&new_chain);
893            let current_work = calculate_chain_work(&current_chain);
894
895            // Only test if both chains have valid work calculations
896            if let (Ok(new_w), Ok(current_w)) = (new_work, current_work) {
897                // Call should_reorganize
898                let should_reorg = should_reorganize(&new_chain, &current_chain).unwrap_or(false);
899
900                // should_reorganize logic:
901                // 1. If new chain is longer, reorganize (regardless of work)
902                // 2. If chains are equal length, reorganize if new chain has more work
903                // 3. Otherwise, don't reorganize
904
905                if new_chain.len() > current_chain.len() {
906                    // New chain is longer - should always reorganize
907                    prop_assert!(should_reorg, "Must reorganize when new chain is longer");
908                } else if new_chain.len() == current_chain.len() {
909                    // Equal length - compare work
910                    if new_w > current_w {
911                        prop_assert!(should_reorg, "Must reorganize when new chain has more work (equal length)");
912                    } else {
913                        prop_assert!(!should_reorg, "Must not reorganize when new chain has less or equal work (equal length)");
914                    }
915                } else {
916                    // New chain is shorter - should not reorganize (regardless of work)
917                    prop_assert!(!should_reorg, "Must not reorganize when new chain is shorter");
918                }
919            }
920            // If either chain has invalid blocks, skip the test (acceptable)
921        }
922    }
923
924    /// Property test: calculate_chain_work is deterministic
925    proptest! {
926        #[test]
927        fn prop_calculate_chain_work_deterministic(
928            chain in proptest::collection::vec(arb_block(), chain_len_range_det())
929        ) {
930            // Calculate work twice - handle errors from invalid blocks
931            let work1 = calculate_chain_work(&chain);
932            let work2 = calculate_chain_work(&chain);
933
934            // Deterministic property: both should succeed or both should fail
935            match (work1, work2) {
936                (Ok(w1), Ok(w2)) => {
937                    prop_assert_eq!(w1, w2, "Chain work calculation must be deterministic");
938                },
939                (Err(_), Err(_)) => {
940                    // Both failed - this is acceptable for invalid blocks
941                },
942                _ => {
943                    prop_assert!(false, "Chain work calculation must be deterministic (both succeed or both fail)");
944                }
945            }
946        }
947    }
948
949    /// Property test: expand_target handles various difficulty values
950    proptest! {
951        #[test]
952        fn prop_expand_target_valid_range(
953            bits in 0x00000000u64..0x1d00ffffu64
954        ) {
955            let result = expand_target(bits);
956
957            match result {
958                Ok(target) => {
959                    // Target can be zero for bits=0, which is valid
960                    // target is u128, so it's always <= u128::MAX (always true)
961                    // This assertion is redundant but kept for documentation
962                    let _ = target;
963                },
964                Err(_) => {
965                    // Some invalid targets may fail, which is acceptable
966                }
967            }
968        }
969    }
970
971    /// Property test: should_reorganize with equal length chains compares work
972    proptest! {
973        #[test]
974        fn prop_should_reorganize_equal_length(
975            chain1 in proptest::collection::vec(arb_block(), 1..3),
976            chain2 in proptest::collection::vec(arb_block(), 1..3)
977        ) {
978            // Ensure equal length
979            let len = chain1.len().min(chain2.len());
980            let chain1 = &chain1[..len];
981            let chain2 = &chain2[..len];
982
983            let work1 = calculate_chain_work(chain1);
984            let work2 = calculate_chain_work(chain2);
985
986            // Only test if both chains have valid work calculations
987            if let (Ok(w1), Ok(w2)) = (work1, work2) {
988                let should_reorg = should_reorganize(chain1, chain2).unwrap_or(false);
989
990                // For equal length chains, reorganize iff chain1 has more work
991                if w1 > w2 {
992                    prop_assert!(should_reorg, "Must reorganize when first chain has more work");
993                } else {
994                    prop_assert!(!should_reorg, "Must not reorganize when first chain has less or equal work");
995                }
996            }
997            // If either chain has invalid blocks, skip the test (acceptable)
998        }
999    }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004    use super::*;
1005
1006    #[test]
1007    fn test_should_reorganize_longer_chain() {
1008        let new_chain = vec![create_test_block(), create_test_block()];
1009        let current_chain = vec![create_test_block()];
1010
1011        assert!(should_reorganize(&new_chain, &current_chain).unwrap());
1012    }
1013
1014    #[test]
1015    fn test_should_reorganize_same_length_more_work() {
1016        let mut new_chain = vec![create_test_block()];
1017        let mut current_chain = vec![create_test_block()];
1018
1019        // Make new chain have lower difficulty (more work)
1020        new_chain[0].header.bits = 0x0200ffff; // Lower difficulty (exponent = 2)
1021        current_chain[0].header.bits = 0x0300ffff; // Higher difficulty (exponent = 3)
1022
1023        assert!(should_reorganize(&new_chain, &current_chain).unwrap());
1024    }
1025
1026    #[test]
1027    fn test_should_not_reorganize_shorter_chain() {
1028        let new_chain = vec![create_test_block()];
1029        let current_chain = vec![create_test_block(), create_test_block()];
1030
1031        assert!(!should_reorganize(&new_chain, &current_chain).unwrap());
1032    }
1033
1034    #[test]
1035    fn test_find_common_ancestor() {
1036        let new_chain = vec![create_test_block()];
1037        let current_chain = vec![create_test_block()];
1038
1039        let ancestor = find_common_ancestor(&new_chain, &current_chain).unwrap();
1040        assert_eq!(ancestor.header.version, 4);
1041        assert_eq!(ancestor.new_chain_index, 0);
1042        assert_eq!(ancestor.current_chain_index, 0);
1043    }
1044
1045    #[test]
1046    fn test_find_common_ancestor_empty_chain() {
1047        let new_chain = vec![];
1048        let current_chain = vec![create_test_block()];
1049
1050        let result = find_common_ancestor(&new_chain, &current_chain);
1051        assert!(result.is_err());
1052    }
1053
1054    #[test]
1055    fn test_calculate_chain_work() {
1056        let mut block = create_test_block();
1057        // Use a bits value with exponent <= 18 so the target fits in u128
1058        block.header.bits = 0x0300ffff;
1059        let chain = vec![block];
1060        let work = calculate_chain_work(&chain).unwrap();
1061        assert!(work > 0);
1062    }
1063
1064    #[test]
1065    fn test_reorganize_chain() {
1066        // Set up a fork: both chains share an ancestor block, then diverge.
1067        // current_chain = [ancestor] (tip at height 1)
1068        // new_chain = [ancestor, new_block] (longer chain, should win)
1069        let ancestor = create_test_block_at_height(0);
1070        let mut new_block = create_test_block_at_height(1);
1071        new_block.header.nonce = 42; // Different block than ancestor
1072                                     // Recalculate merkle root (nonce doesn't affect it, but prev_block_hash irrelevant for connect_block)
1073
1074        let new_chain = vec![ancestor.clone(), new_block];
1075        let current_chain = vec![ancestor];
1076        let utxo_set = UtxoSet::default();
1077
1078        // current_height = 1 (tip of current_chain at height 1)
1079        let result = reorganize_chain(
1080            &new_chain,
1081            &current_chain,
1082            utxo_set,
1083            1,
1084            crate::types::Network::Regtest,
1085        );
1086        match result {
1087            Ok(reorg_result) => {
1088                // Ancestor is at height 1 (current_height - 0 blocks after it = 1)
1089                // One new block connected at height 2
1090                assert_eq!(reorg_result.new_height, 2);
1091                assert_eq!(reorg_result.connected_blocks.len(), 1);
1092                assert_eq!(reorg_result.connected_block_undo_logs.len(), 1);
1093            }
1094            Err(_) => {
1095                // Acceptable: validation may fail for simplified test blocks
1096            }
1097        }
1098    }
1099
1100    #[test]
1101    fn test_reorganize_chain_deep_reorg() {
1102        // Create blocks with unique hashes by varying nonce
1103        let mut block1 = create_test_block();
1104        block1.header.nonce = 1;
1105        let mut block2 = create_test_block();
1106        block2.header.nonce = 2;
1107        let mut block3 = create_test_block();
1108        block3.header.nonce = 3;
1109        let new_chain = vec![block1, block2, block3];
1110
1111        let mut current_block1 = create_test_block();
1112        current_block1.header.nonce = 10;
1113        let mut current_block2 = create_test_block();
1114        current_block2.header.nonce = 11;
1115        let current_chain = vec![current_block1, current_block2];
1116        let utxo_set = UtxoSet::default();
1117
1118        let result = reorganize_chain(
1119            &new_chain,
1120            &current_chain,
1121            utxo_set,
1122            2,
1123            crate::types::Network::Regtest,
1124        );
1125        match result {
1126            Ok(reorg_result) => {
1127                assert_eq!(reorg_result.connected_blocks.len(), 3);
1128                assert_eq!(reorg_result.reorganization_depth, 2);
1129                // Verify undo logs are stored for all connected blocks
1130                assert_eq!(reorg_result.connected_block_undo_logs.len(), 3);
1131            }
1132            Err(_) => {
1133                // Expected failure due to simplified validation
1134            }
1135        }
1136    }
1137
1138    #[test]
1139    fn test_undo_log_storage_and_retrieval() {
1140        use crate::block::connect_block;
1141        use crate::segwit::Witness;
1142
1143        let block = create_test_block_at_height(1);
1144        let mut utxo_set = UtxoSet::default();
1145
1146        // Add some UTXOs that will be spent
1147        let tx_id = calculate_tx_id(&block.transactions[0]);
1148        let outpoint = OutPoint {
1149            hash: tx_id,
1150            index: 0,
1151        };
1152        let utxo = UTXO {
1153            value: 5_000_000_000,
1154            script_pubkey: vec![0x51].into(),
1155            height: 1,
1156            is_coinbase: false,
1157        };
1158        utxo_set.insert(outpoint.clone(), std::sync::Arc::new(utxo.clone()));
1159
1160        // Connect block and get undo log
1161        let witnesses: Vec<Vec<Witness>> = block
1162            .transactions
1163            .iter()
1164            .map(|tx| tx.inputs.iter().map(|_| Vec::new()).collect())
1165            .collect();
1166        let ctx = crate::block::BlockValidationContext::for_network(crate::types::Network::Regtest);
1167        let (result, new_utxo_set, undo_log) =
1168            connect_block(&block, &witnesses, utxo_set.clone(), 1, &ctx).unwrap();
1169
1170        assert!(matches!(result, crate::types::ValidationResult::Valid));
1171
1172        // Verify undo log contains entries
1173        assert!(
1174            !undo_log.entries.is_empty(),
1175            "Undo log should contain entries"
1176        );
1177
1178        // Calculate block hash
1179        let block_hash = calculate_block_hash(&block.header);
1180
1181        // Store undo log in a map (simulating persistent storage)
1182        let mut undo_log_storage: HashMap<Hash, BlockUndoLog> = HashMap::new();
1183        undo_log_storage.insert(block_hash, undo_log.clone());
1184
1185        // Retrieve undo log
1186        let retrieved_undo_log = undo_log_storage.get(&block_hash);
1187        assert!(
1188            retrieved_undo_log.is_some(),
1189            "Should be able to retrieve undo log"
1190        );
1191        assert_eq!(
1192            retrieved_undo_log.unwrap().entries.len(),
1193            undo_log.entries.len()
1194        );
1195
1196        // Disconnect block using retrieved undo log
1197        let disconnected_utxo_set = disconnect_block(&block, &undo_log, new_utxo_set, 1).unwrap();
1198
1199        // Verify UTXO was restored
1200        assert!(
1201            disconnected_utxo_set.contains_key(&outpoint),
1202            "Disconnected UTXO set should contain restored UTXO"
1203        );
1204    }
1205
1206    #[test]
1207    fn test_reorganize_with_undo_log_callback() {
1208        use crate::block::connect_block;
1209        use crate::segwit::Witness;
1210
1211        // Create a block at height 1 and connect it to get undo log
1212        let block = create_test_block_at_height(1);
1213        let utxo_set = UtxoSet::default();
1214        let witnesses: Vec<Vec<Witness>> = block
1215            .transactions
1216            .iter()
1217            .map(|tx| tx.inputs.iter().map(|_| Vec::new()).collect())
1218            .collect();
1219
1220        let ctx = crate::block::BlockValidationContext::for_network(crate::types::Network::Regtest);
1221        let (result, connected_utxo_set, undo_log) =
1222            connect_block(&block, &witnesses, utxo_set.clone(), 1, &ctx).unwrap();
1223
1224        if !matches!(result, crate::types::ValidationResult::Valid) {
1225            eprintln!("Block validation failed: {:?}", result);
1226        }
1227        assert!(matches!(result, crate::types::ValidationResult::Valid));
1228
1229        // Store undo log
1230        let block_hash = calculate_block_hash(&block.header);
1231        let mut undo_log_storage: HashMap<Hash, BlockUndoLog> = HashMap::new();
1232        undo_log_storage.insert(block_hash, undo_log);
1233
1234        // Create callback to retrieve undo log
1235        let get_undo_log =
1236            |hash: &Hash| -> Option<BlockUndoLog> { undo_log_storage.get(hash).cloned() };
1237
1238        // Reorganize with undo log callback
1239        // new_chain shares the same ancestor block, plus a new block at height 2
1240        let mut new_block = create_test_block_at_height(2);
1241        new_block.header.nonce = 42; // Differentiate from ancestor
1242        let new_chain = vec![block.clone(), new_block];
1243        let current_chain = vec![block];
1244        let empty_witnesses: Vec<Vec<Vec<Witness>>> = new_chain
1245            .iter()
1246            .map(|b| {
1247                b.transactions
1248                    .iter()
1249                    .map(|tx| tx.inputs.iter().map(|_| Vec::new()).collect())
1250                    .collect()
1251            })
1252            .collect();
1253
1254        let reorg_result = reorganize_chain_with_witnesses(
1255            &new_chain,
1256            &empty_witnesses,
1257            None,
1258            &current_chain,
1259            connected_utxo_set,
1260            1,
1261            None::<fn(&Block) -> Option<Vec<Witness>>>,
1262            None::<fn(Natural) -> Option<Vec<BlockHeader>>>,
1263            Some(get_undo_log),
1264            None::<fn(&Hash, &BlockUndoLog) -> Result<()>>, // No storage in test
1265            crate::types::Network::Regtest,
1266        );
1267
1268        // Reorganization should succeed (or fail gracefully)
1269        match reorg_result {
1270            Ok(result) => {
1271                // Verify undo logs are stored for new blocks
1272                assert!(!result.connected_block_undo_logs.is_empty());
1273            }
1274            Err(_) => {
1275                // Expected failure due to simplified validation
1276            }
1277        }
1278    }
1279
1280    #[test]
1281    fn test_reorganize_chain_empty_new_chain() {
1282        let new_chain = vec![];
1283        let current_chain = vec![create_test_block()];
1284        let utxo_set = UtxoSet::default();
1285
1286        let result = reorganize_chain(
1287            &new_chain,
1288            &current_chain,
1289            utxo_set,
1290            1,
1291            crate::types::Network::Regtest,
1292        );
1293        assert!(result.is_err());
1294    }
1295
1296    #[test]
1297    fn test_reorganize_chain_empty_current_chain() {
1298        let new_chain = vec![create_test_block()];
1299        let current_chain = vec![];
1300        let utxo_set = UtxoSet::default();
1301
1302        let result = reorganize_chain(
1303            &new_chain,
1304            &current_chain,
1305            utxo_set,
1306            0,
1307            crate::types::Network::Regtest,
1308        );
1309        assert!(result.is_err());
1310    }
1311
1312    #[test]
1313    fn test_disconnect_block() {
1314        let block = create_test_block();
1315        let mut utxo_set = UtxoSet::default();
1316
1317        // Add some UTXOs that will be removed
1318        let tx_id = calculate_tx_id(&block.transactions[0]);
1319        let outpoint = OutPoint {
1320            hash: tx_id,
1321            index: 0,
1322        };
1323        let utxo = UTXO {
1324            value: 50_000_000_000,
1325            script_pubkey: vec![0x51].into(),
1326            height: 1,
1327            is_coinbase: false,
1328        };
1329        utxo_set.insert(outpoint, std::sync::Arc::new(utxo));
1330
1331        // Create an empty undo log for testing (simplified)
1332        let empty_undo_log = BlockUndoLog::new();
1333        let result = disconnect_block(&block, &empty_undo_log, utxo_set, 1);
1334        assert!(result.is_ok());
1335    }
1336
1337    #[test]
1338    fn test_calculate_chain_work_empty_chain() {
1339        let chain = vec![];
1340        let work = calculate_chain_work(&chain).unwrap();
1341        assert_eq!(work, 0);
1342    }
1343
1344    #[test]
1345    fn test_calculate_chain_work_multiple_blocks() {
1346        let mut chain = vec![create_test_block(), create_test_block()];
1347        // Use bits values with exponent <= 18 so targets fit in u128
1348        chain[0].header.bits = 0x0300ffff;
1349        chain[1].header.bits = 0x0200ffff;
1350
1351        let work = calculate_chain_work(&chain).unwrap();
1352        assert!(work > 0);
1353    }
1354
1355    #[test]
1356    fn test_expand_target_edge_cases() {
1357        // Test zero target
1358        let result = expand_target(0x00000000);
1359        assert!(result.is_ok());
1360
1361        // Test maximum valid target
1362        let result = expand_target(0x03ffffff);
1363        assert!(result.is_ok());
1364
1365        // Test invalid target (too large) - use exponent > 19
1366        let result = expand_target(0x14000000); // exponent = 20, which should fail (> 19)
1367        assert!(result.is_err());
1368    }
1369
1370    #[test]
1371    fn test_calculate_tx_id_different_transactions() {
1372        let tx1 = Transaction {
1373            version: 1,
1374            inputs: vec![].into(),
1375            outputs: vec![].into(),
1376            lock_time: 0,
1377        };
1378
1379        let tx2 = Transaction {
1380            version: 2,
1381            inputs: vec![].into(),
1382            outputs: vec![].into(),
1383            lock_time: 0,
1384        };
1385
1386        let id1 = calculate_tx_id(&tx1);
1387        let id2 = calculate_tx_id(&tx2);
1388
1389        assert_ne!(id1, id2);
1390    }
1391
1392    /// Encode a block height into a BIP34-compliant coinbase scriptSig prefix.
1393    /// Follows Bitcoin's CScriptNum serialization:
1394    /// - Height 0: OP_0 (0x00)
1395    /// - Height 1+: push N bytes of little-endian height (with sign-bit padding)
1396    fn encode_bip34_height(height: u64) -> Vec<u8> {
1397        if height == 0 {
1398            // CScriptNum(0) serializes to empty vec, CScript << empty = OP_0
1399            return vec![0x00, 0xff]; // OP_0 + padding to meet 2-byte minimum
1400        }
1401        let mut height_bytes = Vec::new();
1402        let mut n = height;
1403        while n > 0 {
1404            height_bytes.push((n & 0xff) as u8);
1405            n >>= 8;
1406        }
1407        // If high bit is set, add 0x00 for positive sign
1408        if height_bytes.last().map_or(false, |&b| b & 0x80 != 0) {
1409            height_bytes.push(0x00);
1410        }
1411        let mut script_sig = Vec::with_capacity(1 + height_bytes.len() + 1);
1412        script_sig.push(height_bytes.len() as u8); // direct push length
1413        script_sig.extend_from_slice(&height_bytes);
1414        // Pad to at least 2 bytes (coinbase scriptSig minimum)
1415        if script_sig.len() < 2 {
1416            script_sig.push(0xff);
1417        }
1418        script_sig
1419    }
1420
1421    /// Create a test block with BIP34-compliant coinbase encoding for the given height.
1422    fn create_test_block_at_height(height: u64) -> Block {
1423        use crate::mining::calculate_merkle_root;
1424
1425        let script_sig = encode_bip34_height(height);
1426        let coinbase_tx = Transaction {
1427            version: 1,
1428            inputs: vec![TransactionInput {
1429                prevout: OutPoint {
1430                    hash: [0; 32].into(),
1431                    index: 0xffffffff,
1432                },
1433                script_sig,
1434                sequence: 0xffffffff,
1435            }]
1436            .into(),
1437            outputs: vec![TransactionOutput {
1438                value: 5_000_000_000,
1439                script_pubkey: vec![0x51].into(),
1440            }]
1441            .into(),
1442            lock_time: 0,
1443        };
1444
1445        let merkle_root =
1446            calculate_merkle_root(&[coinbase_tx.clone()]).expect("Failed to calculate merkle root");
1447
1448        Block {
1449            header: BlockHeader {
1450                version: 4,
1451                prev_block_hash: [0; 32],
1452                merkle_root,
1453                timestamp: 1231006505,
1454                bits: 0x207fffff, // Regtest difficulty
1455                nonce: 0,
1456            },
1457            transactions: vec![coinbase_tx].into_boxed_slice(),
1458        }
1459    }
1460
1461    /// Create a test block at height 0 (backward-compatible default).
1462    fn create_test_block() -> Block {
1463        create_test_block_at_height(0)
1464    }
1465}