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