Skip to main content

blvm_consensus/
mempool.rs

1//! Mempool validation functions from Orange Paper Section 9
2
3use crate::constants::*;
4use crate::economic::calculate_fee;
5use crate::error::{ConsensusError, Result};
6use crate::script::verify_script;
7use crate::segwit::Witness;
8use crate::transaction::{check_transaction, check_tx_inputs};
9use crate::types::*;
10use blvm_spec_lock::spec_locked;
11use std::collections::HashSet;
12
13/// AcceptToMemoryPool: 𝒯𝒳 × 𝒰𝒮 → {accepted, rejected}
14///
15/// For transaction tx and UTXO set us:
16/// 1. Check if tx is already in mempool
17/// 2. Validate transaction structure
18/// 3. Check inputs against UTXO set
19/// 4. Verify scripts
20/// 5. Check mempool-specific rules (size, fee rate, etc.)
21/// 6. Check for conflicts with existing mempool transactions
22/// 7. Return acceptance result
23///
24/// # Arguments
25///
26/// * `tx` - Transaction to validate
27/// * `witnesses` - Optional witness data for each input (Vec<Witness> where Witness = Vec<ByteString>)
28/// * `utxo_set` - Current UTXO set
29/// * `mempool` - Current mempool state
30/// * `height` - Current block height
31/// * `time_context` - Time context with median time-past of chain tip (BIP113) for transaction finality check
32#[spec_locked("9.1", "AcceptToMemoryPool")]
33pub fn accept_to_memory_pool(
34    tx: &Transaction,
35    witnesses: Option<&[Witness]>,
36    utxo_set: &UtxoSet,
37    mempool: &Mempool,
38    height: Natural,
39    time_context: Option<TimeContext>,
40) -> Result<MempoolResult> {
41    // Precondition assertions: Validate function inputs
42    // Note: We check coinbase and empty transactions and return Rejected rather than asserting,
43    // to allow tests to verify the validation logic properly
44    if tx.inputs.is_empty() && tx.outputs.is_empty() {
45        return Ok(MempoolResult::Rejected(
46            "Transaction must have at least one input or output".to_string(),
47        ));
48    }
49    if is_coinbase(tx) {
50        return Ok(MempoolResult::Rejected(
51            "Coinbase transactions cannot be added to mempool".to_string(),
52        ));
53    }
54    assert!(
55        height <= i64::MAX as u64,
56        "Block height {height} must fit in i64"
57    );
58    assert!(
59        utxo_set.len() <= u32::MAX as usize,
60        "UTXO set size {} exceeds maximum",
61        utxo_set.len()
62    );
63    if let Some(wits) = witnesses {
64        assert!(
65            wits.len() == tx.inputs.len(),
66            "Witness count {} must match input count {}",
67            wits.len(),
68            tx.inputs.len()
69        );
70    }
71
72    // 1. Check if transaction is already in mempool
73    let tx_id = crate::block::calculate_tx_id(tx);
74    // Invariant assertion: Transaction ID must be valid
75    assert!(tx_id != [0u8; 32], "Transaction ID must be non-zero");
76    if mempool.contains(&tx_id) {
77        return Ok(MempoolResult::Rejected(
78            "Transaction already in mempool".to_string(),
79        ));
80    }
81
82    // 2. Validate transaction structure
83    if !matches!(check_transaction(tx)?, ValidationResult::Valid) {
84        return Ok(MempoolResult::Rejected(
85            "Invalid transaction structure".to_string(),
86        ));
87    }
88
89    // 2.5. Check transaction finality
90    // Use median time-past of chain tip (BIP113) for proper locktime/sequence validation
91    let block_time = time_context.map(|ctx| ctx.median_time_past).unwrap_or(0);
92    if !is_final_tx(tx, height, block_time) {
93        return Ok(MempoolResult::Rejected(
94            "Transaction not final (locktime not satisfied)".to_string(),
95        ));
96    }
97
98    // 3. Check inputs against UTXO set
99    let (input_valid, fee) = check_tx_inputs(tx, utxo_set, height)?;
100    // Invariant assertion: Fee must be non-negative
101    assert!(fee >= 0, "Fee {fee} must be non-negative");
102    use crate::constants::MAX_MONEY;
103    assert!(fee <= MAX_MONEY, "Fee {fee} must not exceed MAX_MONEY");
104    if !matches!(input_valid, ValidationResult::Valid) {
105        return Ok(MempoolResult::Rejected(
106            "Invalid transaction inputs".to_string(),
107        ));
108    }
109
110    // 4. Verify scripts for non-coinbase transactions
111    if !is_coinbase(tx) {
112        // Calculate script verification flags
113        // Enable SegWit flag if transaction has witness data
114        let flags = calculate_script_flags(tx, witnesses);
115
116        #[cfg(all(feature = "production", feature = "rayon"))]
117        {
118            use rayon::prelude::*;
119
120            // Optimization: Batch UTXO lookups and parallelize script verification
121            // Pre-lookup all UTXOs to avoid concurrent HashMap access
122            // Pre-allocate with known size
123            let input_utxos: Vec<(usize, Option<&UTXO>)> = {
124                let mut result = Vec::with_capacity(tx.inputs.len());
125                for (i, input) in tx.inputs.iter().enumerate() {
126                    result.push((i, utxo_set.get(&input.prevout).map(|a| a.as_ref())));
127                }
128                result
129            };
130
131            // Parallelize script verification (read-only operations) ✅ Thread-safe
132            let script_results: Result<Vec<bool>> = input_utxos
133                .par_iter()
134                .map(|(i, opt_utxo)| {
135                    if let Some(utxo) = opt_utxo {
136                        let input = &tx.inputs[*i];
137                        let witness: Option<&ByteString> = witnesses
138                            .and_then(|wits| wits.get(*i))
139                            .and_then(|wit| wit.first());
140
141                        verify_script(&input.script_sig, &utxo.script_pubkey, witness, flags)
142                    } else {
143                        Ok(false)
144                    }
145                })
146                .collect();
147
148            // Check results sequentially
149            let script_results = script_results?;
150            // Invariant assertion: Script results count must match input count
151            assert!(
152                script_results.len() == tx.inputs.len(),
153                "Script results count {} must match input count {}",
154                script_results.len(),
155                tx.inputs.len()
156            );
157            for (i, &is_valid) in script_results.iter().enumerate() {
158                // Bounds checking assertion: Input index must be valid
159                assert!(
160                    i < tx.inputs.len(),
161                    "Input index {i} out of bounds in script validation loop",
162                );
163                // Invariant: is_valid is bool from verify_script
164                if !is_valid {
165                    return Ok(MempoolResult::Rejected(format!(
166                        "Invalid script at input {i}"
167                    )));
168                }
169            }
170        }
171
172        #[cfg(not(all(feature = "production", feature = "rayon")))]
173        {
174            // Sequential fallback
175            for (i, input) in tx.inputs.iter().enumerate() {
176                if let Some(utxo) = utxo_set.get(&input.prevout) {
177                    // Get witness for this input if available
178                    // Witness is Vec<ByteString> per input, for verify_script we need Option<&ByteString>
179                    // For SegWit P2WPKH/P2WSH, we typically use the witness stack elements
180                    // For now, we'll use the first element if available (simplified)
181                    let witness: Option<&ByteString> =
182                        witnesses.and_then(|wits| wits.get(i)).and_then(|wit| {
183                            // Witness is Vec<ByteString> - for verify_script we can pass the first element
184                            // or construct a combined witness script. For now, use first element.
185                            wit.first()
186                        });
187
188                    if !verify_script(&input.script_sig, &utxo.script_pubkey, witness, flags)? {
189                        return Ok(MempoolResult::Rejected(format!(
190                            "Invalid script at input {i}"
191                        )));
192                    }
193                }
194            }
195        }
196    }
197
198    // 5. Check mempool-specific rules
199    if !check_mempool_rules(tx, fee, mempool)? {
200        return Ok(MempoolResult::Rejected("Failed mempool rules".to_string()));
201    }
202
203    // 6. Check for conflicts with existing mempool transactions
204    if has_conflicts(tx, mempool)? {
205        return Ok(MempoolResult::Rejected(
206            "Transaction conflicts with mempool".to_string(),
207        ));
208    }
209
210    Ok(MempoolResult::Accepted)
211}
212
213/// Calculate script verification flags based on transaction type
214///
215/// Returns appropriate flags for script validation:
216/// - Base flags: Standard validation flags (P2SH, STRICTENC, DERSIG, LOW_S, etc.)
217/// - SegWit flag (SCRIPT_VERIFY_WITNESS = 0x800): Enabled if transaction uses SegWit
218/// - Taproot flag (SCRIPT_VERIFY_TAPROOT = 0x4000): Enabled if transaction uses Taproot
219fn calculate_script_flags(tx: &Transaction, witnesses: Option<&[Witness]>) -> u32 {
220    // Delegate to the canonical script flag calculation used by block validation.
221    //
222    // Note: For mempool policy we only care about which flags are enabled, not the
223    // actual witness contents here, so we rely on the transaction structure itself
224    // (including SegWit/Taproot outputs) in `calculate_script_flags_for_block`.
225    // Witness data is still threaded through to `verify_script` separately.
226    //
227    // For mempool policy, we use a height that activates all soft forks (well past all activations).
228    // This ensures we validate using the most strict rules.
229    // Check if witness data is present (optimization: just check bool, no witness needed)
230    let has_witness = witnesses.map(|w| !w.is_empty()).unwrap_or(false);
231    const MEMPOOL_POLICY_HEIGHT: u64 = 1_000_000; // All soft forks active at this height
232    crate::block::calculate_script_flags_for_block_network(
233        tx,
234        has_witness,
235        MEMPOOL_POLICY_HEIGHT,
236        crate::types::Network::Mainnet,
237    )
238}
239
240/// IsStandardTx: 𝒯𝒳 → {true, false}
241///
242/// Check if transaction follows standard rules for mempool acceptance:
243/// 1. Transaction size limits
244/// 2. Script size limits
245/// 3. Standard script types
246/// 4. Fee rate requirements
247#[spec_locked("9.2", "IsStandardTx")]
248pub fn is_standard_tx(tx: &Transaction) -> Result<bool> {
249    // 1. Check transaction size
250    let tx_size = calculate_transaction_size(tx);
251    if tx_size > MAX_TX_SIZE {
252        return Ok(false);
253    }
254
255    // 2. Check script sizes
256    for (i, input) in tx.inputs.iter().enumerate() {
257        // Bounds checking assertion: Input index must be valid
258        assert!(i < tx.inputs.len(), "Input index {i} out of bounds");
259        // Invariant assertion: Script size must be reasonable
260        assert!(
261            input.script_sig.len() <= MAX_SCRIPT_SIZE * 2,
262            "Script size {} must be reasonable for input {}",
263            input.script_sig.len(),
264            i
265        );
266        if input.script_sig.len() > MAX_SCRIPT_SIZE {
267            return Ok(false);
268        }
269    }
270
271    for (i, output) in tx.outputs.iter().enumerate() {
272        // Bounds checking assertion: Output index must be valid
273        assert!(i < tx.outputs.len(), "Output index {i} out of bounds");
274        // Invariant assertion: Script size must be reasonable
275        assert!(
276            output.script_pubkey.len() <= MAX_SCRIPT_SIZE * 2,
277            "Script size {} must be reasonable for output {}",
278            output.script_pubkey.len(),
279            i
280        );
281        if output.script_pubkey.len() > MAX_SCRIPT_SIZE {
282            return Ok(false);
283        }
284    }
285
286    // 3. Check for standard script types (simplified)
287    for (i, output) in tx.outputs.iter().enumerate() {
288        // Bounds checking assertion: Output index must be valid
289        assert!(
290            i < tx.outputs.len(),
291            "Output index {i} out of bounds in standard check"
292        );
293        if !is_standard_script(&output.script_pubkey)? {
294            return Ok(false);
295        }
296    }
297
298    // Postcondition assertion: Result must be boolean
299    let result = true;
300    // Note: Result is boolean (tautology for formal verification)
301    Ok(result)
302}
303
304/// ReplacementChecks: 𝒯𝒳 × 𝒯𝒳 × 𝒰𝒮 × Mempool → {true, false}
305///
306/// Check if new transaction can replace existing one (BIP125 RBF rules).
307///
308/// According to BIP125 and Orange Paper Section 9.3, replacement is allowed if:
309/// 1. Existing transaction signals RBF (nSequence < SEQUENCE_FINAL)
310/// 2. New transaction has higher fee rate: FeeRate(tx_2) > FeeRate(tx_1)
311/// 3. New transaction pays absolute fee bump: Fee(tx_2) > Fee(tx_1) + MIN_RELAY_FEE
312/// 4. New transaction conflicts with existing: tx_2 spends at least one input from tx_1
313/// 5. No new unconfirmed dependencies: All inputs of tx_2 are confirmed or from tx_1
314#[spec_locked("9.3", "ReplacementChecks")]
315pub fn replacement_checks(
316    new_tx: &Transaction,
317    existing_tx: &Transaction,
318    utxo_set: &UtxoSet,
319    mempool: &Mempool,
320) -> Result<bool> {
321    // Precondition checks: Validate function inputs
322    // Note: We check these conditions and return an error rather than asserting,
323    // to allow tests to verify the validation logic properly
324    // Bitcoin requires transactions to have both inputs and outputs (except coinbase)
325    if new_tx.inputs.is_empty() && new_tx.outputs.is_empty() {
326        return Err(crate::error::ConsensusError::ConsensusRuleViolation(
327            "New transaction must have at least one input or output"
328                .to_string()
329                .into(),
330        ));
331    }
332    if existing_tx.inputs.is_empty() && existing_tx.outputs.is_empty() {
333        return Err(crate::error::ConsensusError::ConsensusRuleViolation(
334            "Existing transaction must have at least one input or output"
335                .to_string()
336                .into(),
337        ));
338    }
339    if is_coinbase(new_tx) {
340        return Err(crate::error::ConsensusError::ConsensusRuleViolation(
341            "New transaction cannot be coinbase".to_string().into(),
342        ));
343    }
344    if is_coinbase(existing_tx) {
345        return Err(crate::error::ConsensusError::ConsensusRuleViolation(
346            "Existing transaction cannot be coinbase".to_string().into(),
347        ));
348    }
349    assert!(
350        utxo_set.len() <= u32::MAX as usize,
351        "UTXO set size {} exceeds maximum",
352        utxo_set.len()
353    );
354
355    // 1. Check RBF signaling - existing transaction must signal RBF
356    // Note: new_tx doesn't need to signal RBF per BIP125, only existing_tx does
357    if !signals_rbf(existing_tx) {
358        return Ok(false);
359    }
360
361    // 2. Check fee rate: FeeRate(tx_2) > FeeRate(tx_1)
362    let new_fee = calculate_fee(new_tx, utxo_set)?;
363    let existing_fee = calculate_fee(existing_tx, utxo_set)?;
364    // Invariant assertion: Fees must be non-negative
365    assert!(new_fee >= 0, "New fee {new_fee} must be non-negative");
366    assert!(
367        existing_fee >= 0,
368        "Existing fee {existing_fee} must be non-negative"
369    );
370    use crate::constants::MAX_MONEY;
371    assert!(
372        new_fee <= MAX_MONEY,
373        "New fee {new_fee} must not exceed MAX_MONEY"
374    );
375    assert!(
376        existing_fee <= MAX_MONEY,
377        "Existing fee {existing_fee} must not exceed MAX_MONEY"
378    );
379
380    let new_tx_size = calculate_transaction_size_vbytes(new_tx);
381    let existing_tx_size = calculate_transaction_size_vbytes(existing_tx);
382    // Invariant assertion: Transaction sizes must be positive
383    assert!(
384        new_tx_size > 0,
385        "New transaction size {new_tx_size} must be positive"
386    );
387    assert!(
388        existing_tx_size > 0,
389        "Existing transaction size {existing_tx_size} must be positive"
390    );
391    assert!(
392        new_tx_size <= MAX_TX_SIZE * 2,
393        "New transaction size {new_tx_size} must be reasonable"
394    );
395    assert!(
396        existing_tx_size <= MAX_TX_SIZE * 2,
397        "Existing transaction size {existing_tx_size} must be reasonable"
398    );
399
400    if new_tx_size == 0 || existing_tx_size == 0 {
401        return Ok(false);
402    }
403
404    // Use integer-based comparison to avoid floating-point precision issues
405    // Compare: new_fee / new_tx_size > existing_fee / existing_tx_size
406    // Equivalent to: new_fee * existing_tx_size > existing_fee * new_tx_size
407    // This avoids floating-point division and precision errors
408
409    // Runtime assertion: Transaction sizes must be positive
410    debug_assert!(
411        new_tx_size > 0,
412        "New transaction size ({new_tx_size}) must be positive"
413    );
414    debug_assert!(
415        existing_tx_size > 0,
416        "Existing transaction size ({existing_tx_size}) must be positive"
417    );
418
419    // Use integer multiplication to avoid floating-point precision issues
420    // Check: new_fee * existing_tx_size > existing_fee * new_tx_size
421    let new_fee_scaled = (new_fee as u128)
422        .checked_mul(existing_tx_size as u128)
423        .ok_or_else(|| {
424            ConsensusError::TransactionValidation("Fee rate calculation overflow".into())
425        })?;
426    let existing_fee_scaled = (existing_fee as u128)
427        .checked_mul(new_tx_size as u128)
428        .ok_or_else(|| {
429            ConsensusError::TransactionValidation("Fee rate calculation overflow".into())
430        })?;
431
432    if new_fee_scaled <= existing_fee_scaled {
433        return Ok(false);
434    }
435
436    // 3. Check absolute fee bump: Fee(tx_2) > Fee(tx_1) + MIN_RELAY_FEE
437    if new_fee <= existing_fee + MIN_RELAY_FEE {
438        return Ok(false);
439    }
440
441    // 4. Check conflict: tx_2 must spend at least one input from tx_1
442    if !has_conflict_with_tx(new_tx, existing_tx) {
443        return Ok(false);
444    }
445
446    // 5. Check for new unconfirmed dependencies
447    // All inputs of tx_2 must be confirmed (in UTXO set) or from tx_1
448    if creates_new_dependencies(new_tx, existing_tx, utxo_set, mempool)? {
449        return Ok(false);
450    }
451
452    Ok(true)
453}
454
455// ============================================================================
456// HELPER FUNCTIONS
457// ============================================================================
458
459/// Mempool data structure
460pub type Mempool = HashSet<Hash>;
461
462/// Result of mempool acceptance
463#[derive(Debug, Clone, PartialEq, Eq)]
464pub enum MempoolResult {
465    Accepted,
466    Rejected(String),
467}
468
469/// Update mempool after block connection
470///
471/// Removes transactions that were included in the block and transactions
472/// that became invalid due to spent inputs.
473///
474/// This function should be called after successfully connecting a block
475/// to keep the mempool synchronized with the blockchain state.
476///
477/// # Arguments
478///
479/// * `mempool` - Mutable reference to the mempool
480/// * `block` - The block that was just connected
481/// * `utxo_set` - The updated UTXO set after block connection
482///
483/// # Returns
484///
485/// Returns a vector of transaction IDs that were removed from the mempool.
486///
487/// # Example
488///
489/// ```rust
490/// use blvm_consensus::mempool::{Mempool, update_mempool_after_block};
491/// use blvm_consensus::block::{connect_block, BlockValidationContext};
492/// use blvm_consensus::ValidationResult;
493///
494/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
495/// # use blvm_consensus::types::*;
496/// # use blvm_consensus::mining::calculate_merkle_root;
497/// # let coinbase_tx = Transaction {
498/// #     version: 1,
499/// #     inputs: vec![TransactionInput {
500/// #         prevout: OutPoint { hash: [0; 32].into(), index: 0xffffffff },
501/// #         script_sig: vec![],
502/// #         sequence: 0xffffffff,
503/// #     }].into(),
504/// #     outputs: vec![TransactionOutput { value: 5000000000, script_pubkey: vec![].into() }].into(),
505/// #     lock_time: 0,
506/// # };
507/// # let merkle_root = calculate_merkle_root(&[coinbase_tx.clone()]).unwrap();
508/// # let block = Block {
509/// #     header: BlockHeader {
510/// #         version: 1, prev_block_hash: [0; 32], merkle_root,
511/// #         timestamp: 1234567890, bits: 0x1d00ffff, nonce: 0,
512/// #     },
513/// #     transactions: vec![coinbase_tx].into(),
514/// # };
515/// // One `Vec<Witness>` per tx; coinbase has one input → one empty witness stack.
516/// # let witnesses: Vec<Vec<blvm_consensus::segwit::Witness>> = vec![vec![vec![]]];
517/// # let mut utxo_set = UtxoSet::default();
518/// # let height = 0;
519/// # let mut mempool = Mempool::new();
520/// let ctx = BlockValidationContext::for_network(Network::Regtest);
521/// let (result, new_utxo_set, _) = connect_block(&block, &witnesses, utxo_set, height, &ctx)?;
522/// if matches!(result, ValidationResult::Valid) {
523///     let removed = update_mempool_after_block(&mut mempool, &block, &new_utxo_set)?;
524///     println!("Removed {} transactions from mempool", removed.len());
525/// }
526/// # Ok(())
527/// # }
528/// ```
529pub fn update_mempool_after_block(
530    mempool: &mut Mempool,
531    block: &crate::types::Block,
532    _utxo_set: &crate::types::UtxoSet,
533) -> Result<Vec<Hash>> {
534    let mut removed = Vec::new();
535
536    // 1. Remove transactions that were included in the block
537    for tx in &block.transactions {
538        let tx_id = crate::block::calculate_tx_id(tx);
539        if mempool.remove(&tx_id) {
540            removed.push(tx_id);
541        }
542    }
543
544    // 2. Remove transactions that became invalid (inputs were spent by block)
545    // Note: We don't have the transaction here, just the ID
546    // In a full implementation, we'd need a transaction store or lookup
547    // For now, we'll skip this check and rely on the caller to handle it
548    // Use `update_mempool_after_block_with_lookup` for full validation
549    // This is a limitation that should be addressed with a transaction index
550
551    Ok(removed)
552}
553
554/// Update mempool after block connection (with transaction lookup)
555///
556/// This is a more complete version that can check if mempool transactions
557/// became invalid. Requires a way to look up transactions by ID.
558///
559/// # Arguments
560///
561/// * `mempool` - Mutable reference to the mempool
562/// * `block` - The block that was just connected
563/// * `get_tx_by_id` - Function to look up transactions by ID
564///
565/// # Returns
566///
567/// Returns a vector of transaction IDs that were removed from the mempool.
568pub fn update_mempool_after_block_with_lookup<F>(
569    mempool: &mut Mempool,
570    block: &crate::types::Block,
571    get_tx_by_id: F,
572) -> Result<Vec<Hash>>
573where
574    F: Fn(&Hash) -> Option<crate::types::Transaction>,
575{
576    let mut removed = Vec::new();
577
578    // 1. Remove transactions that were included in the block
579    for tx in &block.transactions {
580        let tx_id = crate::block::calculate_tx_id(tx);
581        if mempool.remove(&tx_id) {
582            removed.push(tx_id);
583        }
584    }
585
586    // 2. Remove transactions that became invalid (inputs were spent by block)
587    // Collect spent outpoints from the block
588    let mut spent_outpoints = std::collections::HashSet::new();
589    for tx in &block.transactions {
590        if !crate::transaction::is_coinbase(tx) {
591            for input in &tx.inputs {
592                spent_outpoints.insert(input.prevout);
593            }
594        }
595    }
596
597    // Check each mempool transaction to see if it spends any of the spent outpoints
598    let mut invalid_tx_ids = Vec::new();
599    for &tx_id in mempool.iter() {
600        if let Some(tx) = get_tx_by_id(&tx_id) {
601            // Check if any input of this transaction was spent by the block
602            for input in &tx.inputs {
603                if spent_outpoints.contains(&input.prevout) {
604                    invalid_tx_ids.push(tx_id);
605                    break;
606                }
607            }
608        }
609    }
610
611    // Remove invalid transactions
612    for tx_id in invalid_tx_ids {
613        if mempool.remove(&tx_id) {
614            removed.push(tx_id);
615        }
616    }
617
618    Ok(removed)
619}
620
621/// Check mempool-specific rules
622fn check_mempool_rules(tx: &Transaction, fee: Integer, mempool: &Mempool) -> Result<bool> {
623    // Check minimum fee rate (simplified)
624    let tx_size = calculate_transaction_size(tx);
625    // Use integer-based fee rate calculation to avoid floating-point precision issues
626    // For display purposes, we still use f64, but for comparisons we use integer math
627    // Runtime assertion: Transaction size must be positive
628    debug_assert!(
629        tx_size > 0,
630        "Transaction size ({tx_size}) must be positive for fee rate calculation"
631    );
632
633    let fee_rate = (fee as f64) / (tx_size as f64);
634
635    // Runtime assertion: Fee rate must be non-negative
636    debug_assert!(
637        fee_rate >= 0.0,
638        "Fee rate ({fee_rate:.6}) must be non-negative (fee: {fee}, size: {tx_size})"
639    );
640
641    // Get minimum fee rate from configuration
642    let config = crate::config::get_consensus_config_ref();
643    let min_fee_rate = config.mempool.min_relay_fee_rate as f64; // sat/vB
644    let min_tx_fee = config.mempool.min_tx_fee; // absolute minimum fee
645
646    // Check absolute minimum fee
647    if fee < min_tx_fee {
648        return Ok(false);
649    }
650
651    // Check fee rate (sat/vB)
652    if fee_rate < min_fee_rate {
653        return Ok(false);
654    }
655
656    // Check mempool size limits using configuration
657    // Use transaction count limit (simpler than size-based for now)
658    if mempool.len() > config.mempool.max_mempool_txs {
659        return Ok(false);
660    }
661
662    Ok(true)
663}
664
665/// Check for transaction conflicts
666fn has_conflicts(tx: &Transaction, mempool: &Mempool) -> Result<bool> {
667    // Check if any input is already spent by mempool transaction
668    for input in &tx.inputs {
669        // In a real implementation, we'd check if this input is already spent
670        // by another transaction in the mempool
671        // For now, we'll do a simplified check
672        if mempool.contains(&input.prevout.hash) {
673            return Ok(true);
674        }
675    }
676
677    Ok(false)
678}
679
680/// Check if transaction is final (Orange Paper Section 9.1 - Transaction Finality)
681///
682/// IsFinalTx — locktime/sequence validation (BIP65/68).
683///
684/// A transaction is final if:
685/// 1. tx.lock_time == 0 (no locktime restriction), OR
686/// 2. If locktime < LOCKTIME_THRESHOLD (block height): height > tx.lock_time
687/// 3. If locktime >= LOCKTIME_THRESHOLD (timestamp): block_time > tx.lock_time
688/// 4. OR if all inputs have SEQUENCE_FINAL (0xffffffff), locktime is ignored
689///
690/// Mathematical specification:
691/// ∀ tx ∈ Transaction, height ∈ ℕ, block_time ∈ ℕ:
692/// - is_final_tx(tx, height, block_time) = true ⟹
693///   (tx.lock_time = 0 ∨
694///   (tx.lock_time < LOCKTIME_THRESHOLD ∧ height > tx.lock_time) ∨
695///   (tx.lock_time >= LOCKTIME_THRESHOLD ∧ block_time > tx.lock_time) ∨
696///   (∀ input ∈ tx.inputs: input.sequence == SEQUENCE_FINAL))
697///
698/// Check if transaction is final (Orange Paper Section 9.1 - Transaction Finality)
699///
700/// IsFinalTx — locktime/sequence validation (BIP65/68).
701///
702/// A transaction is final if:
703/// 1. tx.lock_time == 0 (no locktime restriction), OR
704/// 2. If locktime < LOCKTIME_THRESHOLD (block height): height > tx.lock_time
705/// 3. If locktime >= LOCKTIME_THRESHOLD (timestamp): block_time > tx.lock_time
706/// 4. OR if all inputs have SEQUENCE_FINAL (0xffffffff), locktime is ignored
707///
708/// Mathematical specification:
709/// ∀ tx ∈ Transaction, height ∈ ℕ, block_time ∈ ℕ:
710/// - is_final_tx(tx, height, block_time) = true ⟹
711///   (tx.lock_time = 0 ∨
712///   (tx.lock_time < LOCKTIME_THRESHOLD ∧ height > tx.lock_time) ∨
713///   (tx.lock_time >= LOCKTIME_THRESHOLD ∧ block_time > tx.lock_time) ∨
714///   (∀ input ∈ tx.inputs: input.sequence == SEQUENCE_FINAL))
715///
716/// # Arguments
717/// * `tx` - Transaction to check
718/// * `height` - Current block height
719/// * `block_time` - Median time-past of chain tip (BIP113) for timestamp locktime validation
720#[spec_locked("9.1.1", "CheckFinalTxAtTip")]
721pub fn is_final_tx(tx: &Transaction, height: Natural, block_time: Natural) -> bool {
722    use crate::constants::SEQUENCE_FINAL;
723
724    // If locktime is 0, transaction is always final
725    if tx.lock_time == 0 {
726        return true;
727    }
728
729    // Check if locktime is satisfied based on type
730    // If locktime < threshold, compare to block height; else compare to block time
731    // This means: locktime < (condition ? height : block_time)
732    // So: if locktime < threshold, check locktime < height
733    //     if locktime >= threshold, check locktime < block_time
734    let locktime_satisfied = if (tx.lock_time as u32) < LOCKTIME_THRESHOLD {
735        // Block height locktime: check if locktime < height
736        (tx.lock_time as Natural) < height
737    } else {
738        // Timestamp locktime: check if locktime < block_time
739        (tx.lock_time as Natural) < block_time
740    };
741
742    if locktime_satisfied {
743        return true;
744    }
745
746    // Even if locktime isn't satisfied, transaction is final if all inputs have SEQUENCE_FINAL
747    // This allows transactions to bypass locktime by setting all sequences to 0xffffffff
748    // If all inputs have SEQUENCE_FINAL, locktime is ignored
749    for input in &tx.inputs {
750        if (input.sequence as u32) != SEQUENCE_FINAL {
751            return false;
752        }
753    }
754
755    // All inputs have SEQUENCE_FINAL - transaction is final regardless of locktime
756    true
757}
758
759/// Check if transaction signals RBF
760///
761/// Returns true if any input has nSequence < SEQUENCE_FINAL (0xffffffff)
762pub fn signals_rbf(tx: &Transaction) -> bool {
763    for input in &tx.inputs {
764        if (input.sequence as u32) < SEQUENCE_FINAL {
765            return true;
766        }
767    }
768    false
769}
770
771/// Calculate transaction size in virtual bytes (vbytes)
772///
773/// For SegWit transactions, uses weight/4 (virtual bytes).
774/// For non-SegWit transactions, uses byte size.
775/// This is a simplified version - proper implementation would use segwit::calculate_weight()
776fn calculate_transaction_size_vbytes(tx: &Transaction) -> usize {
777    // Simplified: use byte size as approximation
778    // In production, should use proper weight calculation for SegWit
779    calculate_transaction_size(tx)
780}
781
782/// Check if new transaction conflicts with existing transaction
783///
784/// A conflict exists if new_tx spends at least one input from existing_tx.
785/// This is requirement #4 of BIP125.
786pub fn has_conflict_with_tx(new_tx: &Transaction, existing_tx: &Transaction) -> bool {
787    for new_input in &new_tx.inputs {
788        for existing_input in &existing_tx.inputs {
789            if new_input.prevout == existing_input.prevout {
790                return true;
791            }
792        }
793    }
794    false
795}
796
797/// Check if new transaction creates new unconfirmed dependencies
798///
799/// BIP125 requirement #5: All inputs of tx_2 must be:
800/// - Confirmed (in UTXO set), OR
801/// - From tx_1 (spending the same inputs)
802fn creates_new_dependencies(
803    new_tx: &Transaction,
804    existing_tx: &Transaction,
805    utxo_set: &UtxoSet,
806    mempool: &Mempool,
807) -> Result<bool> {
808    for input in &new_tx.inputs {
809        // Check if input is confirmed (in UTXO set)
810        if utxo_set.contains_key(&input.prevout) {
811            continue;
812        }
813
814        // Check if input was spent by existing transaction
815        let mut found_in_existing = false;
816        for existing_input in &existing_tx.inputs {
817            if existing_input.prevout == input.prevout {
818                found_in_existing = true;
819                break;
820            }
821        }
822
823        if found_in_existing {
824            continue;
825        }
826
827        // If not confirmed and not from existing tx, it's a new unconfirmed dependency
828        // Check if it's at least in mempool (but still unconfirmed)
829        if !mempool.contains(&input.prevout.hash) {
830            return Ok(true); // New unconfirmed dependency
831        }
832    }
833
834    Ok(false)
835}
836
837/// Check if script is standard
838fn is_standard_script(script: &ByteString) -> Result<bool> {
839    // Simplified standard script check
840    // In reality, this would check for P2PKH, P2SH, P2WPKH, P2WSH, etc.
841    if script.is_empty() {
842        return Ok(false);
843    }
844
845    // Basic checks
846    if script.len() > MAX_SCRIPT_SIZE {
847        return Ok(false);
848    }
849
850    // Check for non-standard opcodes (simplified)
851    for &byte in script {
852        if byte > 0x60 && byte < 0x7f {
853            // Some non-standard opcodes
854            return Ok(false);
855        }
856    }
857
858    Ok(true)
859}
860
861/// Calculate transaction ID (deprecated - use crate::block::calculate_tx_id instead)
862///
863/// This function is kept for backward compatibility but delegates to the
864/// standard implementation in block.rs.
865#[deprecated(note = "Use crate::block::calculate_tx_id instead")]
866#[spec_locked("5.1", "CalculateTxId")]
867pub fn calculate_tx_id(tx: &Transaction) -> Hash {
868    crate::block::calculate_tx_id(tx)
869}
870
871/// Calculate transaction size (simplified)
872// Use the actual serialization-based size calculation from transaction module
873// Ensures consistency with base serialization size (no witness)
874fn calculate_transaction_size(tx: &Transaction) -> usize {
875    use crate::transaction::calculate_transaction_size as tx_size;
876    tx_size(tx)
877}
878
879/// Check if transaction is coinbase
880fn is_coinbase(tx: &Transaction) -> bool {
881    // Optimization: Use constant folding for zero hash check
882    #[cfg(feature = "production")]
883    {
884        use crate::optimizations::constant_folding::is_zero_hash;
885        tx.inputs.len() == 1
886            && is_zero_hash(&tx.inputs[0].prevout.hash)
887            && tx.inputs[0].prevout.index == 0xffffffff
888    }
889
890    #[cfg(not(feature = "production"))]
891    {
892        tx.inputs.len() == 1
893            && tx.inputs[0].prevout.hash == [0u8; 32]
894            && tx.inputs[0].prevout.index == 0xffffffff
895    }
896}
897
898// ============================================================================
899// FORMAL VERIFICATION
900// ============================================================================
901
902/// Mathematical Specification for Mempool:
903/// ∀ tx ∈ 𝒯𝒳, utxo_set ∈ 𝒰𝒮, mempool ∈ Mempool:
904/// - accept_to_memory_pool(tx, utxo_set, mempool) = Accepted ⟹
905///   (tx ∉ mempool ∧
906///    CheckTransaction(tx) = valid ∧
907///    CheckTxInputs(tx, utxo_set) = valid ∧
908///    VerifyScripts(tx) = valid ∧
909///    ¬has_conflicts(tx, mempool))
910///
911/// Invariants:
912/// - Mempool never contains duplicate transactions
913/// - Mempool never contains conflicting transactions
914/// - Accepted transactions are valid
915/// - RBF rules are enforced
916
917#[cfg(test)]
918mod tests {
919    use super::*;
920    use crate::opcodes::*;
921
922    #[test]
923    fn test_accept_to_memory_pool_valid() {
924        // Skip script validation for now - focus on mempool logic
925        let tx = create_valid_transaction();
926        let utxo_set = create_test_utxo_set();
927        let mempool = Mempool::new();
928
929        // This will fail on script validation, but that's expected
930        let time_context = Some(TimeContext {
931            network_time: 1234567890,
932            median_time_past: 1234567890,
933        });
934        let result =
935            accept_to_memory_pool(&tx, None, &utxo_set, &mempool, 100, time_context).unwrap();
936        assert!(matches!(result, MempoolResult::Rejected(_)));
937    }
938
939    #[test]
940    fn test_accept_to_memory_pool_duplicate() {
941        let tx = create_valid_transaction();
942        let utxo_set = create_test_utxo_set();
943        let mut mempool = Mempool::new();
944        mempool.insert(crate::block::calculate_tx_id(&tx));
945
946        let time_context = Some(TimeContext {
947            network_time: 1234567890,
948            median_time_past: 1234567890,
949        });
950        let result =
951            accept_to_memory_pool(&tx, None, &utxo_set, &mempool, 100, time_context).unwrap();
952        assert!(matches!(result, MempoolResult::Rejected(_)));
953    }
954
955    #[test]
956    fn test_is_standard_tx_valid() {
957        let tx = create_valid_transaction();
958        assert!(is_standard_tx(&tx).unwrap());
959    }
960
961    #[test]
962    fn test_is_standard_tx_too_large() {
963        let mut tx = create_valid_transaction();
964        // Make transaction too large by adding many inputs
965        // MAX_INPUTS (100,000) * ~42 bytes/input >> MAX_TX_SIZE (1,000,000)
966        for _ in 0..MAX_INPUTS {
967            tx.inputs.push(create_dummy_input());
968        }
969        // Transaction exceeds MAX_TX_SIZE so it should NOT be standard
970        assert!(!is_standard_tx(&tx).unwrap());
971    }
972
973    #[test]
974    fn test_replacement_checks_all_requirements() {
975        let utxo_set = create_test_utxo_set();
976        let mempool = Mempool::new();
977
978        // Create existing transaction with RBF signaling and lower fee
979        let mut existing_tx = create_valid_transaction();
980        existing_tx.inputs[0].sequence = SEQUENCE_RBF as u64;
981        existing_tx.outputs[0].value = 9000; // Fee = 10000 - 9000 = 1000 sats
982
983        // Create new transaction that:
984        // 1. Signals RBF (or doesn't - per BIP125 only existing needs to signal)
985        // 2. Conflicts with existing (same input)
986        // 3. Has higher fee rate and absolute fee
987        let mut new_tx = existing_tx.clone();
988        new_tx.outputs[0].value = 8000; // Fee = 10000 - 8000 = 2000 sats
989                                        // Higher fee rate and absolute fee bump (2000 > 1000 + 1000 = 2000, needs >)
990        new_tx.outputs[0].value = 7999; // Fee = 10000 - 7999 = 2001 sats
991
992        // Should pass all BIP125 checks
993        let result = replacement_checks(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap();
994        assert!(result, "Valid RBF replacement should be accepted");
995    }
996
997    #[test]
998    fn test_replacement_checks_no_rbf_signal() {
999        let utxo_set = create_test_utxo_set();
1000        let mempool = Mempool::new();
1001
1002        let new_tx = create_valid_transaction();
1003        let existing_tx = create_valid_transaction(); // No RBF signal
1004
1005        // Should fail: existing transaction doesn't signal RBF
1006        assert!(!replacement_checks(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap());
1007    }
1008
1009    #[test]
1010    fn test_replacement_checks_no_conflict() {
1011        let mut utxo_set = create_test_utxo_set();
1012        // Add UTXO for the new transaction's input
1013        let new_outpoint = OutPoint {
1014            hash: [2; 32],
1015            index: 0,
1016        };
1017        let new_utxo = UTXO {
1018            value: 10000,
1019            script_pubkey: vec![OP_1].into(),
1020            height: 0,
1021            is_coinbase: false,
1022        };
1023        utxo_set.insert(new_outpoint, std::sync::Arc::new(new_utxo));
1024
1025        let mempool = Mempool::new();
1026
1027        let mut existing_tx = create_valid_transaction();
1028        existing_tx.inputs[0].sequence = SEQUENCE_RBF as u64;
1029
1030        // New transaction with different input (no conflict)
1031        let mut new_tx = create_valid_transaction();
1032        new_tx.inputs[0].prevout.hash = [2; 32]; // Different input
1033        new_tx.inputs[0].sequence = SEQUENCE_RBF as u64;
1034        // Ensure output value doesn't exceed input value to avoid negative fee
1035        new_tx.outputs[0].value = 5000; // Less than input value of 10000
1036
1037        // Should fail: no conflict (requirement #4)
1038        assert!(!replacement_checks(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap());
1039    }
1040
1041    #[test]
1042    fn test_replacement_checks_fee_rate_too_low() {
1043        let utxo_set = create_test_utxo_set();
1044        let mempool = Mempool::new();
1045
1046        // Existing transaction with higher fee rate
1047        let mut existing_tx = create_valid_transaction();
1048        existing_tx.inputs[0].sequence = SEQUENCE_RBF as u64;
1049        existing_tx.outputs[0].value = 5000; // Fee = 5000 sats, size = small
1050
1051        // New transaction with same or lower fee rate (but higher absolute fee)
1052        let mut new_tx = existing_tx.clone();
1053        new_tx.outputs[0].value = 4999; // Fee = 5001 sats, but same size so same fee rate
1054
1055        // Should fail: fee rate not higher (requirement #2)
1056        assert!(!replacement_checks(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap());
1057    }
1058
1059    #[test]
1060    fn test_replacement_checks_absolute_fee_insufficient() {
1061        let utxo_set = create_test_utxo_set();
1062        let mempool = Mempool::new();
1063
1064        // Existing transaction
1065        let mut existing_tx = create_valid_transaction();
1066        existing_tx.inputs[0].sequence = SEQUENCE_RBF as u64;
1067        existing_tx.outputs[0].value = 9000; // Fee = 1000 sats
1068
1069        // New transaction with higher fee rate but insufficient absolute fee bump
1070        // Fee must be > 1000 + 1000 = 2000, so need > 2000
1071        let mut new_tx = existing_tx.clone();
1072        new_tx.outputs[0].value = 8001; // Fee = 1999 sats (insufficient)
1073
1074        // Should fail: absolute fee not high enough (requirement #3)
1075        assert!(!replacement_checks(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap());
1076
1077        // Now with sufficient fee
1078        new_tx.outputs[0].value = 7999; // Fee = 2001 sats (sufficient)
1079                                        // Should still fail on other checks (conflict, etc.), but fee check passes
1080                                        // For full test, need to ensure conflict exists
1081    }
1082
1083    // ============================================================================
1084    // COMPREHENSIVE MEMPOOL TESTS
1085    // ============================================================================
1086
1087    #[test]
1088    fn test_accept_to_memory_pool_coinbase() {
1089        let coinbase_tx = create_coinbase_transaction();
1090        let utxo_set = UtxoSet::default();
1091        let mempool = Mempool::new();
1092        // Coinbase transactions should be rejected from mempool
1093        let time_context = Some(TimeContext {
1094            network_time: 0,
1095            median_time_past: 0,
1096        });
1097        let result =
1098            accept_to_memory_pool(&coinbase_tx, None, &utxo_set, &mempool, 100, time_context)
1099                .unwrap();
1100        assert!(matches!(result, MempoolResult::Rejected(_)));
1101    }
1102
1103    #[test]
1104    fn test_is_standard_tx_large_script() {
1105        let mut tx = create_valid_transaction();
1106        // Create a script that's too large
1107        tx.inputs[0].script_sig = vec![OP_1; MAX_SCRIPT_SIZE + 1];
1108
1109        let result = is_standard_tx(&tx).unwrap();
1110        assert!(!result);
1111    }
1112
1113    #[test]
1114    fn test_is_standard_tx_large_output_script() {
1115        let mut tx = create_valid_transaction();
1116        // Create an output script that's too large
1117        tx.outputs[0].script_pubkey = vec![OP_1; MAX_SCRIPT_SIZE + 1];
1118
1119        let result = is_standard_tx(&tx).unwrap();
1120        assert!(!result);
1121    }
1122
1123    #[test]
1124    fn test_replacement_checks_new_unconfirmed_dependency() {
1125        let utxo_set = create_test_utxo_set();
1126        let mempool = Mempool::new();
1127
1128        // Existing transaction
1129        let mut existing_tx = create_valid_transaction();
1130        existing_tx.inputs[0].sequence = SEQUENCE_RBF as u64;
1131
1132        // New transaction that adds a new unconfirmed input
1133        let mut new_tx = existing_tx.clone();
1134        new_tx.inputs.push(TransactionInput {
1135            prevout: OutPoint {
1136                hash: [99; 32],
1137                index: 0,
1138            }, // Not in UTXO set
1139            script_sig: vec![],
1140            sequence: SEQUENCE_RBF as u64,
1141        });
1142        new_tx.outputs[0].value = 7000; // Higher fee
1143
1144        // Should fail: creates new unconfirmed dependency (requirement #5)
1145        assert!(!replacement_checks(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap());
1146    }
1147
1148    #[test]
1149    fn test_has_conflict_with_tx_true() {
1150        let tx1 = create_valid_transaction();
1151        let mut tx2 = create_valid_transaction();
1152        tx2.inputs[0].prevout = tx1.inputs[0].prevout.clone(); // Same input = conflict
1153
1154        assert!(has_conflict_with_tx(&tx2, &tx1));
1155    }
1156
1157    #[test]
1158    fn test_has_conflict_with_tx_false() {
1159        let tx1 = create_valid_transaction();
1160        let mut tx2 = create_valid_transaction();
1161        tx2.inputs[0].prevout.hash = [2; 32]; // Different input = no conflict
1162
1163        assert!(!has_conflict_with_tx(&tx2, &tx1));
1164    }
1165
1166    #[test]
1167    fn test_replacement_checks_minimum_relay_fee() {
1168        let utxo_set = create_test_utxo_set();
1169        let mempool = Mempool::new();
1170
1171        // Existing transaction
1172        let mut existing_tx = create_valid_transaction();
1173        existing_tx.inputs[0].sequence = SEQUENCE_RBF as u64;
1174        existing_tx.outputs[0].value = 9500; // Fee = 500 sats
1175
1176        // New transaction with exactly MIN_RELAY_FEE bump (not enough, need >)
1177        let mut new_tx = existing_tx.clone();
1178        new_tx.outputs[0].value = 8500; // Fee = 1500 sats (1500 > 500 + 1000 = 1500? No, need >)
1179        assert!(!replacement_checks(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap());
1180
1181        // New transaction with sufficient bump
1182        // Fee = 1501 sats (1501 > 500 + 1000 = 1500)
1183        // Conflict detection and fee rate validation are handled by accept_to_memory_pool
1184        new_tx.outputs[0].value = 8499;
1185    }
1186
1187    #[test]
1188    fn test_check_mempool_rules_low_fee() {
1189        let tx = create_valid_transaction();
1190        let fee = 1; // Very low fee
1191        let mempool = Mempool::new();
1192
1193        let result = check_mempool_rules(&tx, fee, &mempool).unwrap();
1194        assert!(!result);
1195    }
1196
1197    #[test]
1198    fn test_check_mempool_rules_high_fee() {
1199        let tx = create_valid_transaction();
1200        let fee = 10000; // High fee
1201        let mempool = Mempool::new();
1202
1203        let result = check_mempool_rules(&tx, fee, &mempool).unwrap();
1204        assert!(result);
1205    }
1206
1207    #[test]
1208    fn test_check_mempool_rules_full_mempool() {
1209        let tx = create_valid_transaction();
1210        let fee = 10000;
1211        let mut mempool = Mempool::new();
1212
1213        // Fill mempool beyond limit with unique hashes
1214        // Default max_mempool_txs is 100,000, so we need to exceed that
1215        for i in 0..100_001 {
1216            let mut hash = [0u8; 32];
1217            hash[0] = (i & 0xff) as u8;
1218            hash[1] = ((i >> 8) & 0xff) as u8;
1219            hash[2] = ((i >> 16) & 0xff) as u8;
1220            hash[3] = ((i >> 24) & 0xff) as u8;
1221            mempool.insert(hash);
1222        }
1223
1224        // Verify mempool is actually full (exceeds max_mempool_txs limit of 100,000)
1225        assert!(mempool.len() > 100_000);
1226
1227        let result = check_mempool_rules(&tx, fee, &mempool).unwrap();
1228        assert!(!result);
1229    }
1230
1231    #[test]
1232    fn test_has_conflicts_no_conflicts() {
1233        let tx = create_valid_transaction();
1234        let mempool = Mempool::new();
1235
1236        let result = has_conflicts(&tx, &mempool).unwrap();
1237        assert!(!result);
1238    }
1239
1240    #[test]
1241    fn test_has_conflicts_with_conflicts() {
1242        let tx = create_valid_transaction();
1243        let mut mempool = Mempool::new();
1244
1245        // Add a conflicting transaction to mempool
1246        mempool.insert(tx.inputs[0].prevout.hash);
1247
1248        let result = has_conflicts(&tx, &mempool).unwrap();
1249        assert!(result);
1250    }
1251
1252    #[test]
1253    fn test_signals_rbf_true() {
1254        let mut tx = create_valid_transaction();
1255        tx.inputs[0].sequence = 0xfffffffe; // RBF signal
1256
1257        assert!(signals_rbf(&tx));
1258    }
1259
1260    #[test]
1261    fn test_signals_rbf_false() {
1262        let tx = create_valid_transaction(); // sequence = 0xffffffff (final)
1263
1264        assert!(!signals_rbf(&tx));
1265    }
1266
1267    #[test]
1268    fn test_calculate_fee_rate() {
1269        let tx = create_valid_transaction();
1270        let utxo_set = create_test_utxo_set();
1271        let fee = calculate_fee(&tx, &utxo_set);
1272
1273        // Fee should be calculable (may be 0 for valid transactions)
1274        assert!(fee.is_ok());
1275    }
1276
1277    #[test]
1278    fn test_creates_new_dependencies_no_new() {
1279        let new_tx = create_valid_transaction();
1280        let existing_tx = create_valid_transaction();
1281        let mempool = Mempool::new();
1282
1283        let utxo_set = create_test_utxo_set();
1284        let result = creates_new_dependencies(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap();
1285        assert!(!result);
1286    }
1287
1288    #[test]
1289    fn test_creates_new_dependencies_with_new() {
1290        let mut new_tx = create_valid_transaction();
1291        let existing_tx = create_valid_transaction();
1292        let mempool = Mempool::new();
1293
1294        // Make new_tx spend a different input
1295        new_tx.inputs[0].prevout.hash = [2; 32];
1296
1297        let utxo_set = create_test_utxo_set();
1298        let result = creates_new_dependencies(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap();
1299        assert!(result);
1300    }
1301
1302    #[test]
1303    fn test_is_standard_script_empty() {
1304        let script = vec![];
1305        let result = is_standard_script(&script).unwrap();
1306        assert!(!result);
1307    }
1308
1309    #[test]
1310    fn test_is_standard_script_too_large() {
1311        let script = vec![OP_1; MAX_SCRIPT_SIZE + 1];
1312        let result = is_standard_script(&script).unwrap();
1313        assert!(!result);
1314    }
1315
1316    #[test]
1317    fn test_is_standard_script_non_standard_opcode() {
1318        let script = vec![OP_VERIF]; // Non-standard opcode (disabled)
1319        let result = is_standard_script(&script).unwrap();
1320        assert!(!result);
1321    }
1322
1323    #[test]
1324    fn test_is_standard_script_valid() {
1325        let script = vec![OP_1];
1326        let result = is_standard_script(&script).unwrap();
1327        assert!(result);
1328    }
1329
1330    #[test]
1331    fn test_calculate_tx_id() {
1332        let tx = create_valid_transaction();
1333        let tx_id = crate::block::calculate_tx_id(&tx);
1334
1335        // Should be a 32-byte hash
1336        assert_eq!(tx_id.len(), 32);
1337
1338        // Same transaction should produce same ID
1339        let tx_id2 = crate::block::calculate_tx_id(&tx);
1340        assert_eq!(tx_id, tx_id2);
1341    }
1342
1343    #[test]
1344    fn test_calculate_tx_id_different_txs() {
1345        let tx1 = create_valid_transaction();
1346        let mut tx2 = tx1.clone();
1347        tx2.version = 2; // Different version
1348
1349        let id1 = crate::block::calculate_tx_id(&tx1);
1350        let id2 = crate::block::calculate_tx_id(&tx2);
1351
1352        assert_ne!(id1, id2);
1353    }
1354
1355    #[test]
1356    fn test_calculate_transaction_size() {
1357        let tx = create_valid_transaction();
1358        let size = calculate_transaction_size(&tx);
1359
1360        assert!(size > 0);
1361
1362        // Size should be deterministic
1363        let size2 = calculate_transaction_size(&tx);
1364        assert_eq!(size, size2);
1365    }
1366
1367    #[test]
1368    fn test_calculate_transaction_size_multiple_inputs_outputs() {
1369        let mut tx = create_valid_transaction();
1370        tx.inputs.push(create_dummy_input());
1371        tx.outputs.push(create_dummy_output());
1372
1373        let size = calculate_transaction_size(&tx);
1374        assert!(size > 0);
1375    }
1376
1377    #[test]
1378    fn test_is_coinbase_true() {
1379        let coinbase_tx = create_coinbase_transaction();
1380        assert!(is_coinbase(&coinbase_tx));
1381    }
1382
1383    #[test]
1384    fn test_is_coinbase_false() {
1385        let regular_tx = create_valid_transaction();
1386        assert!(!is_coinbase(&regular_tx));
1387    }
1388
1389    // Helper functions for tests
1390    fn create_valid_transaction() -> Transaction {
1391        Transaction {
1392            version: 1,
1393            inputs: vec![create_dummy_input()].into(),
1394            outputs: vec![create_dummy_output()].into(),
1395            lock_time: 0,
1396        }
1397    }
1398
1399    fn create_dummy_input() -> TransactionInput {
1400        TransactionInput {
1401            prevout: OutPoint {
1402                hash: [1; 32],
1403                index: 0,
1404            },
1405            script_sig: vec![OP_1],
1406            sequence: 0xffffffff,
1407        }
1408    }
1409
1410    fn create_dummy_output() -> TransactionOutput {
1411        TransactionOutput {
1412            value: 1000,
1413            script_pubkey: vec![OP_1].into(), // OP_1 for valid script
1414        }
1415    }
1416
1417    fn create_test_utxo_set() -> UtxoSet {
1418        let mut utxo_set = UtxoSet::default();
1419        let outpoint = OutPoint {
1420            hash: [1; 32],
1421            index: 0,
1422        };
1423        let utxo = UTXO {
1424            value: 10000,
1425            script_pubkey: vec![OP_1].into(), // OP_1 for valid script
1426            height: 0,
1427            is_coinbase: false,
1428        };
1429        utxo_set.insert(outpoint, std::sync::Arc::new(utxo));
1430        utxo_set
1431    }
1432
1433    fn create_coinbase_transaction() -> Transaction {
1434        Transaction {
1435            version: 1,
1436            inputs: vec![TransactionInput {
1437                prevout: OutPoint {
1438                    hash: [0; 32].into(),
1439                    index: 0xffffffff,
1440                },
1441                script_sig: vec![],
1442                sequence: 0xffffffff,
1443            }]
1444            .into(),
1445            outputs: vec![TransactionOutput {
1446                value: 5000000000,
1447                script_pubkey: vec![].into(),
1448            }]
1449            .into(),
1450            lock_time: 0,
1451        }
1452    }
1453}