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/// ```
382pub fn update_mempool_after_reorg<F>(
383    mempool: &mut crate::mempool::Mempool,
384    reorg_result: &ReorganizationResult,
385    utxo_set: &UtxoSet,
386    get_tx_by_id: Option<F>,
387) -> Result<Vec<Hash>>
388where
389    F: Fn(&Hash) -> Option<Transaction>,
390{
391    use crate::mempool::update_mempool_after_block;
392
393    let mut all_removed = Vec::new();
394
395    // 1. Remove transactions that were included in the new connected blocks
396    for block in &reorg_result.connected_blocks {
397        let removed = update_mempool_after_block(mempool, block, utxo_set)?;
398        all_removed.extend(removed);
399    }
400
401    // 2. Remove transactions that became invalid (their inputs were spent by new chain)
402    // Collect all spent outpoints from the new connected blocks
403    let mut spent_outpoints = std::collections::HashSet::new();
404    for block in &reorg_result.connected_blocks {
405        for tx in &block.transactions {
406            if !crate::transaction::is_coinbase(tx) {
407                for input in &tx.inputs {
408                    spent_outpoints.insert(input.prevout);
409                }
410            }
411        }
412    }
413
414    // If we have transaction lookup, check each mempool transaction
415    if let Some(lookup) = get_tx_by_id {
416        let mut invalid_tx_ids = Vec::new();
417        for &tx_id in mempool.iter() {
418            if let Some(tx) = lookup(&tx_id) {
419                // Check if any input of this transaction was spent by the new chain
420                for input in &tx.inputs {
421                    if spent_outpoints.contains(&input.prevout) {
422                        invalid_tx_ids.push(tx_id);
423                        break;
424                    }
425                }
426            }
427        }
428
429        // Remove invalid transactions
430        for tx_id in invalid_tx_ids {
431            if mempool.remove(&tx_id) {
432                all_removed.push(tx_id);
433            }
434        }
435    }
436
437    // 3. Optionally re-add transactions from disconnected blocks that are still valid
438    // Note: This is a simplified version. In a full implementation, we'd need to:
439    // - Re-validate transactions against the new UTXO set
440    // - Check if they're still valid (not double-spent, scripts still valid, etc.)
441    // - Re-add them to mempool if valid
442    // For now, we skip this step as it requires full transaction re-validation
443
444    Ok(all_removed)
445}
446
447/// Simplified version without transaction lookup
448pub fn update_mempool_after_reorg_simple(
449    mempool: &mut crate::mempool::Mempool,
450    reorg_result: &ReorganizationResult,
451    utxo_set: &UtxoSet,
452) -> Result<Vec<Hash>> {
453    update_mempool_after_reorg(
454        mempool,
455        reorg_result,
456        utxo_set,
457        None::<fn(&Hash) -> Option<Transaction>>,
458    )
459}
460
461/// Common ancestor result with indices in both chains
462struct CommonAncestorResult {
463    header: BlockHeader,
464    new_chain_index: usize,
465    current_chain_index: usize,
466}
467
468/// Find common ancestor between two chains by comparing block hashes
469///
470/// Algorithm: Start from the tips of both chains and work backwards,
471/// comparing blocks at the same distance from tip until we find a match.
472/// This is the common ancestor where the chains diverged.
473/// Orange Paper 11.3: Chain reorganization finds common ancestor before disconnect/connect.
474fn find_common_ancestor(
475    new_chain: &[Block],
476    current_chain: &[Block],
477) -> Result<CommonAncestorResult> {
478    if new_chain.is_empty() || current_chain.is_empty() {
479        return Err(crate::error::ConsensusError::ConsensusRuleViolation(
480            "Cannot find common ancestor: empty chain".into(),
481        ));
482    }
483
484    // Find the minimum chain length - we can only compare up to this point
485    let min_len = new_chain.len().min(current_chain.len());
486
487    // Work backwards from tips, comparing blocks at the same distance from tip
488    // Distance 0 = tip, distance 1 = one before tip, etc.
489    for distance_from_tip in 0..min_len {
490        let new_idx = new_chain.len() - 1 - distance_from_tip;
491        let current_idx = current_chain.len() - 1 - distance_from_tip;
492
493        let new_hash = calculate_block_hash(&new_chain[new_idx].header);
494        let current_hash = calculate_block_hash(&current_chain[current_idx].header);
495
496        // If hashes match, we found the common ancestor
497        if new_hash == current_hash {
498            return Ok(CommonAncestorResult {
499                header: new_chain[new_idx].header.clone(),
500                new_chain_index: new_idx,
501                current_chain_index: current_idx,
502            });
503        }
504    }
505
506    // If we've checked all blocks up to min_len and none match,
507    // check if genesis blocks match (they should always be the same)
508    if !new_chain.is_empty() && !current_chain.is_empty() {
509        let new_genesis_hash = calculate_block_hash(&new_chain[0].header);
510        let current_genesis_hash = calculate_block_hash(&current_chain[0].header);
511        if new_genesis_hash == current_genesis_hash {
512            return Ok(CommonAncestorResult {
513                header: new_chain[0].header.clone(),
514                new_chain_index: 0,
515                current_chain_index: 0,
516            });
517        }
518    }
519
520    // Chains don't share a common ancestor (should never happen in Bitcoin)
521    Err(crate::error::ConsensusError::ConsensusRuleViolation(
522        "Chains do not share a common ancestor".into(),
523    ))
524}
525
526/// Disconnect a block from the chain (reverse of ConnectBlock)
527///
528/// Uses the undo log to perfectly restore the UTXO set to its state before the block was connected.
529/// This is the inverse operation of `connect_block`.
530/// Orange Paper 11.3.1: DisconnectBlock applies undo log to reverse ConnectBlock.
531///
532/// # Arguments
533///
534/// * `block` - The block to disconnect (used for validation, undo_log contains the actual changes)
535/// * `undo_log` - The undo log created when this block was connected
536/// * `utxo_set` - Current UTXO set (will be modified)
537/// * `_height` - Block height (for potential future use)
538#[spec_locked("11.3.1", "DisconnectBlock")]
539fn disconnect_block(
540    _block: &Block,
541    undo_log: &BlockUndoLog,
542    mut utxo_set: UtxoSet,
543    _height: Natural,
544) -> Result<UtxoSet> {
545    // Precondition assertions: Validate function inputs
546    assert!(
547        !_block.transactions.is_empty(),
548        "Block must have at least one transaction"
549    );
550    assert!(
551        _height <= i64::MAX as u64,
552        "Block height {_height} must fit in i64"
553    );
554    assert!(
555        utxo_set.len() <= u32::MAX as usize,
556        "UTXO set size {} must not exceed maximum",
557        utxo_set.len()
558    );
559    // Invariant assertion: Undo log entry count must be reasonable
560    assert!(
561        undo_log.entries.len() <= 10_000,
562        "Undo log entry count {} must be reasonable",
563        undo_log.entries.len()
564    );
565
566    // Process undo entries in reverse order (most recent first)
567    // This reverses the order of operations from connect_block
568    for (i, entry) in undo_log.entries.iter().enumerate() {
569        // Bounds checking assertion: Entry index must be valid
570        assert!(i < undo_log.entries.len(), "Entry index {i} out of bounds");
571        // Remove new UTXO (if it was created by this block)
572        if entry.new_utxo.is_some() {
573            utxo_set.remove(&entry.outpoint);
574        }
575
576        // Restore previous UTXO (if it was spent by this block)
577        if let Some(previous_utxo) = &entry.previous_utxo {
578            utxo_set.insert(entry.outpoint, std::sync::Arc::clone(previous_utxo));
579        }
580    }
581
582    Ok(utxo_set)
583}
584
585/// Check if reorganization is beneficial
586#[track_caller] // Better error messages showing caller location
587#[allow(clippy::redundant_comparisons)] // Intentional assertions for formal verification
588#[spec_locked("11.3", "ShouldReorganize")]
589pub fn should_reorganize(new_chain: &[Block], current_chain: &[Block]) -> Result<bool> {
590    // Precondition assertions: Validate function inputs
591    assert!(
592        new_chain.len() <= 10_000,
593        "New chain length {} must be reasonable",
594        new_chain.len()
595    );
596    assert!(
597        current_chain.len() <= 10_000,
598        "Current chain length {} must be reasonable",
599        current_chain.len()
600    );
601
602    // Reorganize if new chain is longer
603    if new_chain.len() > current_chain.len() {
604        // Postcondition assertion: Result must be boolean
605        #[allow(clippy::eq_op)]
606        {
607            assert!(true == true || false == false, "Result must be boolean");
608        }
609        return Ok(true);
610    }
611
612    // Reorganize if chains are same length but new chain has more work
613    if new_chain.len() == current_chain.len() {
614        let new_work = calculate_chain_work(new_chain)?;
615        let current_work = calculate_chain_work(current_chain)?;
616        let result = new_work > current_work;
617        // Postcondition assertion: Result must be boolean
618        // Note: Result is boolean (tautology for formal verification)
619        return Ok(result);
620    }
621
622    // Postcondition assertion: Result must be boolean
623    let result = false;
624    // Note: Result is boolean (tautology for formal verification)
625    Ok(result)
626}
627
628/// Calculate total work for a chain
629/// Orange Paper 11.3: BestChain = argmax Σ Work(block)
630///
631/// Mathematical invariants:
632/// - Work is always non-negative
633/// - Work increases monotonically with chain length
634/// - Work calculation is deterministic
635#[spec_locked("11.3", "CalculateChainWork")]
636fn calculate_chain_work(chain: &[Block]) -> Result<u128> {
637    let mut total_work = 0u128;
638
639    for block in chain {
640        let target = expand_target(block.header.bits)?;
641        // Work is proportional to 1/target
642        // Avoid overflow by using checked arithmetic
643        if target > 0 {
644            // Calculate work contribution safely
645            // Work = 2^256 / (target + 1) for Bitcoin
646            // For simplicity, use: work = u128::MAX / (target + 1)
647            // Prevent division by zero and overflow
648            let work_contribution = if target == u128::MAX {
649                0 // Very large target means very small work
650            } else {
651                // Use checked_div to avoid panic, fallback to 0 on overflow
652                u128::MAX.checked_div(target + 1).unwrap_or(0)
653            };
654
655            // u128 is always non-negative - no assertion needed
656
657            let old_total = total_work;
658            total_work = total_work.saturating_add(work_contribution);
659
660            // Runtime assertion: Total work must be non-decreasing
661            debug_assert!(
662                total_work >= old_total,
663                "Total work ({total_work}) must be >= previous total ({old_total})"
664            );
665        }
666        // Zero target means infinite difficulty - skip this block (work = 0)
667    }
668
669    // u128 is always non-negative - no assertion needed
670
671    Ok(total_work)
672}
673
674/// Expand target from compact format (reused from mining module)
675fn expand_target(bits: Natural) -> Result<u128> {
676    let exponent = (bits >> 24) as u8;
677    let mantissa = bits & 0x00ffffff;
678
679    if exponent <= 3 {
680        let shift = 8 * (3 - exponent);
681        Ok((mantissa as u128) >> shift)
682    } else {
683        // Prevent overflow by checking exponent before calculating shift
684        // Maximum safe exponent: 3 + (128 / 8) = 19
685        if exponent > 19 {
686            return Err(crate::error::ConsensusError::InvalidProofOfWork(
687                "Target too large".into(),
688            ));
689        }
690        // Calculate shift safely - exponent is bounded, so no overflow
691        let shift = 8 * (exponent - 3);
692        // Use checked shift to avoid overflow
693        let mantissa_u128 = mantissa as u128;
694        let expanded = mantissa_u128.checked_shl(shift as u32).ok_or_else(|| {
695            crate::error::ConsensusError::InvalidProofOfWork("Target expansion overflow".into())
696        })?;
697        Ok(expanded)
698    }
699}
700
701/// Calculate transaction ID (simplified)
702#[allow(dead_code)] // Used in tests
703fn calculate_tx_id(tx: &Transaction) -> Hash {
704    let mut hash = [0u8; 32];
705    hash[0] = (tx.version & 0xff) as u8;
706    hash[1] = (tx.inputs.len() & 0xff) as u8;
707    hash[2] = (tx.outputs.len() & 0xff) as u8;
708    hash[3] = (tx.lock_time & 0xff) as u8;
709    hash
710}
711
712/// Calculate block hash for indexing undo logs
713///
714/// Uses the block header to compute a unique identifier for the block.
715/// This is used to store and retrieve undo logs during reorganization.
716fn calculate_block_hash(header: &BlockHeader) -> Hash {
717    use sha2::{Digest, Sha256};
718
719    // Serialize block header (80 bytes: version, prev_block_hash, merkle_root, timestamp, bits, nonce)
720    let mut bytes = Vec::with_capacity(80);
721    bytes.extend_from_slice(&header.version.to_le_bytes());
722    bytes.extend_from_slice(&header.prev_block_hash);
723    bytes.extend_from_slice(&header.merkle_root);
724    bytes.extend_from_slice(&header.timestamp.to_le_bytes());
725    bytes.extend_from_slice(&header.bits.to_le_bytes());
726    bytes.extend_from_slice(&header.nonce.to_le_bytes());
727
728    // Double SHA256 (Bitcoin standard)
729    let first_hash = Sha256::digest(&bytes);
730    let second_hash = Sha256::digest(first_hash);
731
732    let mut hash = [0u8; 32];
733    hash.copy_from_slice(&second_hash);
734    hash
735}
736
737// ============================================================================
738// TYPES
739// ============================================================================
740
741mod undo_entry_serde {
742    use crate::types::UTXO;
743    use serde::{Deserialize, Deserializer, Serialize, Serializer};
744    use std::sync::Arc;
745
746    pub fn serialize<S>(opt: &Option<Arc<UTXO>>, s: S) -> Result<S::Ok, S::Error>
747    where
748        S: Serializer,
749    {
750        opt.as_ref().map(|a| a.as_ref()).serialize(s)
751    }
752
753    pub fn deserialize<'de, D>(d: D) -> Result<Option<Arc<UTXO>>, D::Error>
754    where
755        D: Deserializer<'de>,
756    {
757        Option::<UTXO>::deserialize(d).map(|opt| opt.map(Arc::new))
758    }
759}
760
761/// Undo log entry for a single UTXO change
762///
763/// Records the state of a UTXO before and after a transaction is applied.
764/// This allows perfect reversal of UTXO set changes during block disconnection.
765#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
766pub struct UndoEntry {
767    /// The outpoint that was changed
768    pub outpoint: OutPoint,
769    /// The UTXO that existed before (None if it was created by this transaction)
770    #[serde(with = "undo_entry_serde")]
771    pub previous_utxo: Option<std::sync::Arc<UTXO>>,
772    /// The UTXO that exists after (None if it was spent by this transaction)
773    #[serde(with = "undo_entry_serde")]
774    pub new_utxo: Option<std::sync::Arc<UTXO>>,
775}
776
777/// Undo log for a single block
778///
779/// Contains all UTXO changes made by a block, allowing perfect reversal
780/// of the block's effects on the UTXO set.
781///
782/// Entries are stored in reverse order (most recent first) to allow
783/// efficient undo by iterating forward.
784#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
785pub struct BlockUndoLog {
786    /// Entries in reverse order (most recent first)
787    /// This allows efficient undo: iterate forward and restore previous_utxo, remove new_utxo
788    pub entries: Vec<UndoEntry>,
789}
790
791impl BlockUndoLog {
792    /// Create an empty undo log
793    pub fn new() -> Self {
794        Self {
795            entries: Vec::new(),
796        }
797    }
798
799    /// Add an undo entry to the log
800    pub fn push(&mut self, entry: UndoEntry) {
801        self.entries.push(entry);
802    }
803
804    /// Check if the undo log is empty
805    pub fn is_empty(&self) -> bool {
806        self.entries.is_empty()
807    }
808}
809
810impl Default for BlockUndoLog {
811    fn default() -> Self {
812        Self::new()
813    }
814}
815
816/// Result of chain reorganization
817#[derive(Debug, Clone)]
818pub struct ReorganizationResult {
819    pub new_utxo_set: UtxoSet,
820    pub new_height: Natural,
821    pub common_ancestor: BlockHeader,
822    pub disconnected_blocks: Vec<Block>,
823    pub connected_blocks: Vec<Block>,
824    pub reorganization_depth: usize,
825    /// Undo logs for connected blocks (keyed by block hash)
826    /// These can be used for future disconnections
827    pub connected_block_undo_logs: HashMap<Hash, BlockUndoLog>,
828}
829
830// ============================================================================
831// FORMAL VERIFICATION
832// ============================================================================
833
834/// Mathematical Specification for Chain Selection:
835/// ∀ chains C₁, C₂: work(C₁) > work(C₂) ⇒ select(C₁)
836///
837/// Invariants:
838/// - Selected chain has maximum cumulative work
839/// - Work calculation is deterministic
840/// - Empty chains are rejected
841/// - Chain work is always non-negative
842
843#[cfg(test)]
844mod property_tests {
845    use super::*;
846    use proptest::prelude::*;
847
848    /// Helper to get chain length range based on coverage mode
849    fn chain_len_range() -> std::ops::Range<usize> {
850        if std::env::var("CARGO_TARPAULIN").is_ok() || std::env::var("TARPAULIN").is_ok() {
851            1..3 // Reduced range under coverage
852        } else {
853            1..5
854        }
855    }
856
857    /// Helper to get chain length range for deterministic test
858    fn chain_len_range_det() -> std::ops::Range<usize> {
859        if std::env::var("CARGO_TARPAULIN").is_ok() || std::env::var("TARPAULIN").is_ok() {
860            0..3 // Reduced range under coverage
861        } else {
862            0..10
863        }
864    }
865
866    /// Random compact-encoded header bits (same domain as `expand_target` tests).
867    fn arb_block() -> impl Strategy<Value = Block> {
868        (0x00000000u64..0x1d00ffffu64).prop_map(|bits| Block {
869            header: BlockHeader {
870                version: 1,
871                prev_block_hash: [0u8; 32],
872                merkle_root: [0u8; 32],
873                timestamp: 0,
874                bits,
875                nonce: 0,
876            },
877            transactions: Box::new([]),
878        })
879    }
880
881    /// Property test: should_reorganize selects chain with maximum work
882    proptest! {
883        #[test]
884        fn prop_should_reorganize_max_work(
885            new_chain in proptest::collection::vec(arb_block(), chain_len_range()),
886            current_chain in proptest::collection::vec(arb_block(), chain_len_range())
887        ) {
888            // Calculate work for both chains - handle errors from invalid blocks
889            let new_work = calculate_chain_work(&new_chain);
890            let current_work = calculate_chain_work(&current_chain);
891
892            // Only test if both chains have valid work calculations
893            if let (Ok(new_w), Ok(current_w)) = (new_work, current_work) {
894                // Call should_reorganize
895                let should_reorg = should_reorganize(&new_chain, &current_chain).unwrap_or(false);
896
897                // should_reorganize logic:
898                // 1. If new chain is longer, reorganize (regardless of work)
899                // 2. If chains are equal length, reorganize if new chain has more work
900                // 3. Otherwise, don't reorganize
901
902                if new_chain.len() > current_chain.len() {
903                    // New chain is longer - should always reorganize
904                    prop_assert!(should_reorg, "Must reorganize when new chain is longer");
905                } else if new_chain.len() == current_chain.len() {
906                    // Equal length - compare work
907                    if new_w > current_w {
908                        prop_assert!(should_reorg, "Must reorganize when new chain has more work (equal length)");
909                    } else {
910                        prop_assert!(!should_reorg, "Must not reorganize when new chain has less or equal work (equal length)");
911                    }
912                } else {
913                    // New chain is shorter - should not reorganize (regardless of work)
914                    prop_assert!(!should_reorg, "Must not reorganize when new chain is shorter");
915                }
916            }
917            // If either chain has invalid blocks, skip the test (acceptable)
918        }
919    }
920
921    /// Property test: calculate_chain_work is deterministic
922    proptest! {
923        #[test]
924        fn prop_calculate_chain_work_deterministic(
925            chain in proptest::collection::vec(arb_block(), chain_len_range_det())
926        ) {
927            // Calculate work twice - handle errors from invalid blocks
928            let work1 = calculate_chain_work(&chain);
929            let work2 = calculate_chain_work(&chain);
930
931            // Deterministic property: both should succeed or both should fail
932            match (work1, work2) {
933                (Ok(w1), Ok(w2)) => {
934                    prop_assert_eq!(w1, w2, "Chain work calculation must be deterministic");
935                },
936                (Err(_), Err(_)) => {
937                    // Both failed - this is acceptable for invalid blocks
938                },
939                _ => {
940                    prop_assert!(false, "Chain work calculation must be deterministic (both succeed or both fail)");
941                }
942            }
943        }
944    }
945
946    /// Property test: expand_target handles various difficulty values
947    proptest! {
948        #[test]
949        fn prop_expand_target_valid_range(
950            bits in 0x00000000u64..0x1d00ffffu64
951        ) {
952            let result = expand_target(bits);
953
954            match result {
955                Ok(target) => {
956                    // Target can be zero for bits=0, which is valid
957                    // target is u128, so it's always <= u128::MAX (always true)
958                    // This assertion is redundant but kept for documentation
959                    let _ = target;
960                },
961                Err(_) => {
962                    // Some invalid targets may fail, which is acceptable
963                }
964            }
965        }
966    }
967
968    /// Property test: should_reorganize with equal length chains compares work
969    proptest! {
970        #[test]
971        fn prop_should_reorganize_equal_length(
972            chain1 in proptest::collection::vec(arb_block(), 1..3),
973            chain2 in proptest::collection::vec(arb_block(), 1..3)
974        ) {
975            // Ensure equal length
976            let len = chain1.len().min(chain2.len());
977            let chain1 = &chain1[..len];
978            let chain2 = &chain2[..len];
979
980            let work1 = calculate_chain_work(chain1);
981            let work2 = calculate_chain_work(chain2);
982
983            // Only test if both chains have valid work calculations
984            if let (Ok(w1), Ok(w2)) = (work1, work2) {
985                let should_reorg = should_reorganize(chain1, chain2).unwrap_or(false);
986
987                // For equal length chains, reorganize iff chain1 has more work
988                if w1 > w2 {
989                    prop_assert!(should_reorg, "Must reorganize when first chain has more work");
990                } else {
991                    prop_assert!(!should_reorg, "Must not reorganize when first chain has less or equal work");
992                }
993            }
994            // If either chain has invalid blocks, skip the test (acceptable)
995        }
996    }
997}
998
999#[cfg(test)]
1000mod tests {
1001    use super::*;
1002
1003    #[test]
1004    fn test_should_reorganize_longer_chain() {
1005        let new_chain = vec![create_test_block(), create_test_block()];
1006        let current_chain = vec![create_test_block()];
1007
1008        assert!(should_reorganize(&new_chain, &current_chain).unwrap());
1009    }
1010
1011    #[test]
1012    fn test_should_reorganize_same_length_more_work() {
1013        let mut new_chain = vec![create_test_block()];
1014        let mut current_chain = vec![create_test_block()];
1015
1016        // Make new chain have lower difficulty (more work)
1017        new_chain[0].header.bits = 0x0200ffff; // Lower difficulty (exponent = 2)
1018        current_chain[0].header.bits = 0x0300ffff; // Higher difficulty (exponent = 3)
1019
1020        assert!(should_reorganize(&new_chain, &current_chain).unwrap());
1021    }
1022
1023    #[test]
1024    fn test_should_not_reorganize_shorter_chain() {
1025        let new_chain = vec![create_test_block()];
1026        let current_chain = vec![create_test_block(), create_test_block()];
1027
1028        assert!(!should_reorganize(&new_chain, &current_chain).unwrap());
1029    }
1030
1031    #[test]
1032    fn test_find_common_ancestor() {
1033        let new_chain = vec![create_test_block()];
1034        let current_chain = vec![create_test_block()];
1035
1036        let ancestor = find_common_ancestor(&new_chain, &current_chain).unwrap();
1037        assert_eq!(ancestor.header.version, 4);
1038        assert_eq!(ancestor.new_chain_index, 0);
1039        assert_eq!(ancestor.current_chain_index, 0);
1040    }
1041
1042    #[test]
1043    fn test_find_common_ancestor_empty_chain() {
1044        let new_chain = vec![];
1045        let current_chain = vec![create_test_block()];
1046
1047        let result = find_common_ancestor(&new_chain, &current_chain);
1048        assert!(result.is_err());
1049    }
1050
1051    #[test]
1052    fn test_calculate_chain_work() {
1053        let mut block = create_test_block();
1054        // Use a bits value with exponent <= 18 so the target fits in u128
1055        block.header.bits = 0x0300ffff;
1056        let chain = vec![block];
1057        let work = calculate_chain_work(&chain).unwrap();
1058        assert!(work > 0);
1059    }
1060
1061    #[test]
1062    fn test_reorganize_chain() {
1063        // Set up a fork: both chains share an ancestor block, then diverge.
1064        // current_chain = [ancestor] (tip at height 1)
1065        // new_chain = [ancestor, new_block] (longer chain, should win)
1066        let ancestor = create_test_block_at_height(0);
1067        let mut new_block = create_test_block_at_height(1);
1068        new_block.header.nonce = 42; // Different block than ancestor
1069                                     // Recalculate merkle root (nonce doesn't affect it, but prev_block_hash irrelevant for connect_block)
1070
1071        let new_chain = vec![ancestor.clone(), new_block];
1072        let current_chain = vec![ancestor];
1073        let utxo_set = UtxoSet::default();
1074
1075        // current_height = 1 (tip of current_chain at height 1)
1076        let result = reorganize_chain(
1077            &new_chain,
1078            &current_chain,
1079            utxo_set,
1080            1,
1081            crate::types::Network::Regtest,
1082        );
1083        match result {
1084            Ok(reorg_result) => {
1085                // Ancestor is at height 1 (current_height - 0 blocks after it = 1)
1086                // One new block connected at height 2
1087                assert_eq!(reorg_result.new_height, 2);
1088                assert_eq!(reorg_result.connected_blocks.len(), 1);
1089                assert_eq!(reorg_result.connected_block_undo_logs.len(), 1);
1090            }
1091            Err(_) => {
1092                // Acceptable: validation may fail for simplified test blocks
1093            }
1094        }
1095    }
1096
1097    #[test]
1098    fn test_reorganize_chain_deep_reorg() {
1099        // Create blocks with unique hashes by varying nonce
1100        let mut block1 = create_test_block();
1101        block1.header.nonce = 1;
1102        let mut block2 = create_test_block();
1103        block2.header.nonce = 2;
1104        let mut block3 = create_test_block();
1105        block3.header.nonce = 3;
1106        let new_chain = vec![block1, block2, block3];
1107
1108        let mut current_block1 = create_test_block();
1109        current_block1.header.nonce = 10;
1110        let mut current_block2 = create_test_block();
1111        current_block2.header.nonce = 11;
1112        let current_chain = vec![current_block1, current_block2];
1113        let utxo_set = UtxoSet::default();
1114
1115        let result = reorganize_chain(
1116            &new_chain,
1117            &current_chain,
1118            utxo_set,
1119            2,
1120            crate::types::Network::Regtest,
1121        );
1122        match result {
1123            Ok(reorg_result) => {
1124                assert_eq!(reorg_result.connected_blocks.len(), 3);
1125                assert_eq!(reorg_result.reorganization_depth, 2);
1126                // Verify undo logs are stored for all connected blocks
1127                assert_eq!(reorg_result.connected_block_undo_logs.len(), 3);
1128            }
1129            Err(_) => {
1130                // Expected failure due to simplified validation
1131            }
1132        }
1133    }
1134
1135    #[test]
1136    fn test_undo_log_storage_and_retrieval() {
1137        use crate::block::connect_block;
1138        use crate::segwit::Witness;
1139
1140        let block = create_test_block_at_height(1);
1141        let mut utxo_set = UtxoSet::default();
1142
1143        // Add some UTXOs that will be spent
1144        let tx_id = calculate_tx_id(&block.transactions[0]);
1145        let outpoint = OutPoint {
1146            hash: tx_id,
1147            index: 0,
1148        };
1149        let utxo = UTXO {
1150            value: 5_000_000_000,
1151            script_pubkey: vec![0x51].into(),
1152            height: 1,
1153            is_coinbase: false,
1154        };
1155        utxo_set.insert(outpoint.clone(), std::sync::Arc::new(utxo.clone()));
1156
1157        // Connect block and get undo log
1158        let witnesses: Vec<Vec<Witness>> = block
1159            .transactions
1160            .iter()
1161            .map(|tx| tx.inputs.iter().map(|_| Vec::new()).collect())
1162            .collect();
1163        let ctx = crate::block::BlockValidationContext::for_network(crate::types::Network::Regtest);
1164        let (result, new_utxo_set, undo_log) =
1165            connect_block(&block, &witnesses, utxo_set.clone(), 1, &ctx).unwrap();
1166
1167        assert!(matches!(result, crate::types::ValidationResult::Valid));
1168
1169        // Verify undo log contains entries
1170        assert!(
1171            !undo_log.entries.is_empty(),
1172            "Undo log should contain entries"
1173        );
1174
1175        // Calculate block hash
1176        let block_hash = calculate_block_hash(&block.header);
1177
1178        // Store undo log in a map (simulating persistent storage)
1179        let mut undo_log_storage: HashMap<Hash, BlockUndoLog> = HashMap::new();
1180        undo_log_storage.insert(block_hash, undo_log.clone());
1181
1182        // Retrieve undo log
1183        let retrieved_undo_log = undo_log_storage.get(&block_hash);
1184        assert!(
1185            retrieved_undo_log.is_some(),
1186            "Should be able to retrieve undo log"
1187        );
1188        assert_eq!(
1189            retrieved_undo_log.unwrap().entries.len(),
1190            undo_log.entries.len()
1191        );
1192
1193        // Disconnect block using retrieved undo log
1194        let disconnected_utxo_set = disconnect_block(&block, &undo_log, new_utxo_set, 1).unwrap();
1195
1196        // Verify UTXO was restored
1197        assert!(
1198            disconnected_utxo_set.contains_key(&outpoint),
1199            "Disconnected UTXO set should contain restored UTXO"
1200        );
1201    }
1202
1203    #[test]
1204    fn test_reorganize_with_undo_log_callback() {
1205        use crate::block::connect_block;
1206        use crate::segwit::Witness;
1207
1208        // Create a block at height 1 and connect it to get undo log
1209        let block = create_test_block_at_height(1);
1210        let utxo_set = UtxoSet::default();
1211        let witnesses: Vec<Vec<Witness>> = block
1212            .transactions
1213            .iter()
1214            .map(|tx| tx.inputs.iter().map(|_| Vec::new()).collect())
1215            .collect();
1216
1217        let ctx = crate::block::BlockValidationContext::for_network(crate::types::Network::Regtest);
1218        let (result, connected_utxo_set, undo_log) =
1219            connect_block(&block, &witnesses, utxo_set.clone(), 1, &ctx).unwrap();
1220
1221        if !matches!(result, crate::types::ValidationResult::Valid) {
1222            eprintln!("Block validation failed: {:?}", result);
1223        }
1224        assert!(matches!(result, crate::types::ValidationResult::Valid));
1225
1226        // Store undo log
1227        let block_hash = calculate_block_hash(&block.header);
1228        let mut undo_log_storage: HashMap<Hash, BlockUndoLog> = HashMap::new();
1229        undo_log_storage.insert(block_hash, undo_log);
1230
1231        // Create callback to retrieve undo log
1232        let get_undo_log =
1233            |hash: &Hash| -> Option<BlockUndoLog> { undo_log_storage.get(hash).cloned() };
1234
1235        // Reorganize with undo log callback
1236        // new_chain shares the same ancestor block, plus a new block at height 2
1237        let mut new_block = create_test_block_at_height(2);
1238        new_block.header.nonce = 42; // Differentiate from ancestor
1239        let new_chain = vec![block.clone(), new_block];
1240        let current_chain = vec![block];
1241        let empty_witnesses: Vec<Vec<Vec<Witness>>> = new_chain
1242            .iter()
1243            .map(|b| {
1244                b.transactions
1245                    .iter()
1246                    .map(|tx| tx.inputs.iter().map(|_| Vec::new()).collect())
1247                    .collect()
1248            })
1249            .collect();
1250
1251        let reorg_result = reorganize_chain_with_witnesses(
1252            &new_chain,
1253            &empty_witnesses,
1254            None,
1255            &current_chain,
1256            connected_utxo_set,
1257            1,
1258            None::<fn(&Block) -> Option<Vec<Witness>>>,
1259            None::<fn(Natural) -> Option<Vec<BlockHeader>>>,
1260            Some(get_undo_log),
1261            None::<fn(&Hash, &BlockUndoLog) -> Result<()>>, // No storage in test
1262            crate::types::Network::Regtest,
1263        );
1264
1265        // Reorganization should succeed (or fail gracefully)
1266        match reorg_result {
1267            Ok(result) => {
1268                // Verify undo logs are stored for new blocks
1269                assert!(!result.connected_block_undo_logs.is_empty());
1270            }
1271            Err(_) => {
1272                // Expected failure due to simplified validation
1273            }
1274        }
1275    }
1276
1277    #[test]
1278    fn test_reorganize_chain_empty_new_chain() {
1279        let new_chain = vec![];
1280        let current_chain = vec![create_test_block()];
1281        let utxo_set = UtxoSet::default();
1282
1283        let result = reorganize_chain(
1284            &new_chain,
1285            &current_chain,
1286            utxo_set,
1287            1,
1288            crate::types::Network::Regtest,
1289        );
1290        assert!(result.is_err());
1291    }
1292
1293    #[test]
1294    fn test_reorganize_chain_empty_current_chain() {
1295        let new_chain = vec![create_test_block()];
1296        let current_chain = vec![];
1297        let utxo_set = UtxoSet::default();
1298
1299        let result = reorganize_chain(
1300            &new_chain,
1301            &current_chain,
1302            utxo_set,
1303            0,
1304            crate::types::Network::Regtest,
1305        );
1306        assert!(result.is_err());
1307    }
1308
1309    #[test]
1310    fn test_disconnect_block() {
1311        let block = create_test_block();
1312        let mut utxo_set = UtxoSet::default();
1313
1314        // Add some UTXOs that will be removed
1315        let tx_id = calculate_tx_id(&block.transactions[0]);
1316        let outpoint = OutPoint {
1317            hash: tx_id,
1318            index: 0,
1319        };
1320        let utxo = UTXO {
1321            value: 50_000_000_000,
1322            script_pubkey: vec![0x51].into(),
1323            height: 1,
1324            is_coinbase: false,
1325        };
1326        utxo_set.insert(outpoint, std::sync::Arc::new(utxo));
1327
1328        // Create an empty undo log for testing (simplified)
1329        let empty_undo_log = BlockUndoLog::new();
1330        let result = disconnect_block(&block, &empty_undo_log, utxo_set, 1);
1331        assert!(result.is_ok());
1332    }
1333
1334    #[test]
1335    fn test_calculate_chain_work_empty_chain() {
1336        let chain = vec![];
1337        let work = calculate_chain_work(&chain).unwrap();
1338        assert_eq!(work, 0);
1339    }
1340
1341    #[test]
1342    fn test_calculate_chain_work_multiple_blocks() {
1343        let mut chain = vec![create_test_block(), create_test_block()];
1344        // Use bits values with exponent <= 18 so targets fit in u128
1345        chain[0].header.bits = 0x0300ffff;
1346        chain[1].header.bits = 0x0200ffff;
1347
1348        let work = calculate_chain_work(&chain).unwrap();
1349        assert!(work > 0);
1350    }
1351
1352    #[test]
1353    fn test_expand_target_edge_cases() {
1354        // Test zero target
1355        let result = expand_target(0x00000000);
1356        assert!(result.is_ok());
1357
1358        // Test maximum valid target
1359        let result = expand_target(0x03ffffff);
1360        assert!(result.is_ok());
1361
1362        // Test invalid target (too large) - use exponent > 19
1363        let result = expand_target(0x14000000); // exponent = 20, which should fail (> 19)
1364        assert!(result.is_err());
1365    }
1366
1367    #[test]
1368    fn test_calculate_tx_id_different_transactions() {
1369        let tx1 = Transaction {
1370            version: 1,
1371            inputs: vec![].into(),
1372            outputs: vec![].into(),
1373            lock_time: 0,
1374        };
1375
1376        let tx2 = Transaction {
1377            version: 2,
1378            inputs: vec![].into(),
1379            outputs: vec![].into(),
1380            lock_time: 0,
1381        };
1382
1383        let id1 = calculate_tx_id(&tx1);
1384        let id2 = calculate_tx_id(&tx2);
1385
1386        assert_ne!(id1, id2);
1387    }
1388
1389    /// Encode a block height into a BIP34-compliant coinbase scriptSig prefix.
1390    /// Follows Bitcoin's CScriptNum serialization:
1391    /// - Height 0: OP_0 (0x00)
1392    /// - Height 1+: push N bytes of little-endian height (with sign-bit padding)
1393    fn encode_bip34_height(height: u64) -> Vec<u8> {
1394        if height == 0 {
1395            // CScriptNum(0) serializes to empty vec, CScript << empty = OP_0
1396            return vec![0x00, 0xff]; // OP_0 + padding to meet 2-byte minimum
1397        }
1398        let mut height_bytes = Vec::new();
1399        let mut n = height;
1400        while n > 0 {
1401            height_bytes.push((n & 0xff) as u8);
1402            n >>= 8;
1403        }
1404        // If high bit is set, add 0x00 for positive sign
1405        if height_bytes.last().map_or(false, |&b| b & 0x80 != 0) {
1406            height_bytes.push(0x00);
1407        }
1408        let mut script_sig = Vec::with_capacity(1 + height_bytes.len() + 1);
1409        script_sig.push(height_bytes.len() as u8); // direct push length
1410        script_sig.extend_from_slice(&height_bytes);
1411        // Pad to at least 2 bytes (coinbase scriptSig minimum)
1412        if script_sig.len() < 2 {
1413            script_sig.push(0xff);
1414        }
1415        script_sig
1416    }
1417
1418    /// Create a test block with BIP34-compliant coinbase encoding for the given height.
1419    fn create_test_block_at_height(height: u64) -> Block {
1420        use crate::mining::calculate_merkle_root;
1421
1422        let script_sig = encode_bip34_height(height);
1423        let coinbase_tx = Transaction {
1424            version: 1,
1425            inputs: vec![TransactionInput {
1426                prevout: OutPoint {
1427                    hash: [0; 32].into(),
1428                    index: 0xffffffff,
1429                },
1430                script_sig,
1431                sequence: 0xffffffff,
1432            }]
1433            .into(),
1434            outputs: vec![TransactionOutput {
1435                value: 5_000_000_000,
1436                script_pubkey: vec![0x51].into(),
1437            }]
1438            .into(),
1439            lock_time: 0,
1440        };
1441
1442        let merkle_root =
1443            calculate_merkle_root(&[coinbase_tx.clone()]).expect("Failed to calculate merkle root");
1444
1445        Block {
1446            header: BlockHeader {
1447                version: 4,
1448                prev_block_hash: [0; 32],
1449                merkle_root,
1450                timestamp: 1231006505,
1451                bits: 0x207fffff, // Regtest difficulty
1452                nonce: 0,
1453            },
1454            transactions: vec![coinbase_tx].into_boxed_slice(),
1455        }
1456    }
1457
1458    /// Create a test block at height 0 (backward-compatible default).
1459    fn create_test_block() -> Block {
1460        create_test_block_at_height(0)
1461    }
1462}