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")]
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")]
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")]
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    assert!(!is_coinbase(new_tx), "New transaction cannot be coinbase");
340    assert!(
341        !is_coinbase(existing_tx),
342        "Existing transaction cannot be coinbase"
343    );
344    assert!(
345        utxo_set.len() <= u32::MAX as usize,
346        "UTXO set size {} exceeds maximum",
347        utxo_set.len()
348    );
349
350    // 1. Check RBF signaling - existing transaction must signal RBF
351    // Note: new_tx doesn't need to signal RBF per BIP125, only existing_tx does
352    if !signals_rbf(existing_tx) {
353        return Ok(false);
354    }
355
356    // 2. Check fee rate: FeeRate(tx_2) > FeeRate(tx_1)
357    let new_fee = calculate_fee(new_tx, utxo_set)?;
358    let existing_fee = calculate_fee(existing_tx, utxo_set)?;
359    // Invariant assertion: Fees must be non-negative
360    assert!(new_fee >= 0, "New fee {new_fee} must be non-negative");
361    assert!(
362        existing_fee >= 0,
363        "Existing fee {existing_fee} must be non-negative"
364    );
365    use crate::constants::MAX_MONEY;
366    assert!(
367        new_fee <= MAX_MONEY,
368        "New fee {new_fee} must not exceed MAX_MONEY"
369    );
370    assert!(
371        existing_fee <= MAX_MONEY,
372        "Existing fee {existing_fee} must not exceed MAX_MONEY"
373    );
374
375    let new_tx_size = calculate_transaction_size_vbytes(new_tx);
376    let existing_tx_size = calculate_transaction_size_vbytes(existing_tx);
377    // Invariant assertion: Transaction sizes must be positive
378    assert!(
379        new_tx_size > 0,
380        "New transaction size {new_tx_size} must be positive"
381    );
382    assert!(
383        existing_tx_size > 0,
384        "Existing transaction size {existing_tx_size} must be positive"
385    );
386    assert!(
387        new_tx_size <= MAX_TX_SIZE * 2,
388        "New transaction size {new_tx_size} must be reasonable"
389    );
390    assert!(
391        existing_tx_size <= MAX_TX_SIZE * 2,
392        "Existing transaction size {existing_tx_size} must be reasonable"
393    );
394
395    if new_tx_size == 0 || existing_tx_size == 0 {
396        return Ok(false);
397    }
398
399    // Use integer-based comparison to avoid floating-point precision issues
400    // Compare: new_fee / new_tx_size > existing_fee / existing_tx_size
401    // Equivalent to: new_fee * existing_tx_size > existing_fee * new_tx_size
402    // This avoids floating-point division and precision errors
403
404    // Runtime assertion: Transaction sizes must be positive
405    debug_assert!(
406        new_tx_size > 0,
407        "New transaction size ({new_tx_size}) must be positive"
408    );
409    debug_assert!(
410        existing_tx_size > 0,
411        "Existing transaction size ({existing_tx_size}) must be positive"
412    );
413
414    // Use integer multiplication to avoid floating-point precision issues
415    // Check: new_fee * existing_tx_size > existing_fee * new_tx_size
416    let new_fee_scaled = (new_fee as u128)
417        .checked_mul(existing_tx_size as u128)
418        .ok_or_else(|| {
419            ConsensusError::TransactionValidation("Fee rate calculation overflow".into())
420        })?;
421    let existing_fee_scaled = (existing_fee as u128)
422        .checked_mul(new_tx_size as u128)
423        .ok_or_else(|| {
424            ConsensusError::TransactionValidation("Fee rate calculation overflow".into())
425        })?;
426
427    if new_fee_scaled <= existing_fee_scaled {
428        return Ok(false);
429    }
430
431    // 3. Check absolute fee bump: Fee(tx_2) > Fee(tx_1) + MIN_RELAY_FEE
432    if new_fee <= existing_fee + MIN_RELAY_FEE {
433        return Ok(false);
434    }
435
436    // 4. Check conflict: tx_2 must spend at least one input from tx_1
437    if !has_conflict_with_tx(new_tx, existing_tx) {
438        return Ok(false);
439    }
440
441    // 5. Check for new unconfirmed dependencies
442    // All inputs of tx_2 must be confirmed (in UTXO set) or from tx_1
443    if creates_new_dependencies(new_tx, existing_tx, utxo_set, mempool)? {
444        return Ok(false);
445    }
446
447    Ok(true)
448}
449
450// ============================================================================
451// HELPER FUNCTIONS
452// ============================================================================
453
454/// Mempool data structure
455pub type Mempool = HashSet<Hash>;
456
457/// Result of mempool acceptance
458#[derive(Debug, Clone, PartialEq, Eq)]
459pub enum MempoolResult {
460    Accepted,
461    Rejected(String),
462}
463
464/// Update mempool after block connection
465///
466/// Removes transactions that were included in the block and transactions
467/// that became invalid due to spent inputs.
468///
469/// This function should be called after successfully connecting a block
470/// to keep the mempool synchronized with the blockchain state.
471///
472/// # Arguments
473///
474/// * `mempool` - Mutable reference to the mempool
475/// * `block` - The block that was just connected
476/// * `utxo_set` - The updated UTXO set after block connection
477///
478/// # Returns
479///
480/// Returns a vector of transaction IDs that were removed from the mempool.
481///
482/// # Example
483///
484/// ```rust
485/// use blvm_consensus::mempool::{Mempool, update_mempool_after_block};
486/// use blvm_consensus::block::{connect_block, BlockValidationContext};
487/// use blvm_consensus::ValidationResult;
488///
489/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
490/// # use blvm_consensus::types::*;
491/// # use blvm_consensus::mining::calculate_merkle_root;
492/// # let coinbase_tx = Transaction {
493/// #     version: 1,
494/// #     inputs: vec![TransactionInput {
495/// #         prevout: OutPoint { hash: [0; 32].into(), index: 0xffffffff },
496/// #         script_sig: vec![],
497/// #         sequence: 0xffffffff,
498/// #     }].into(),
499/// #     outputs: vec![TransactionOutput { value: 5000000000, script_pubkey: vec![].into() }].into(),
500/// #     lock_time: 0,
501/// # };
502/// # let merkle_root = calculate_merkle_root(&[coinbase_tx.clone()]).unwrap();
503/// # let block = Block {
504/// #     header: BlockHeader {
505/// #         version: 1, prev_block_hash: [0; 32], merkle_root,
506/// #         timestamp: 1234567890, bits: 0x1d00ffff, nonce: 0,
507/// #     },
508/// #     transactions: vec![coinbase_tx].into(),
509/// # };
510/// // One `Vec<Witness>` per tx; coinbase has one input → one empty witness stack.
511/// # let witnesses: Vec<Vec<blvm_consensus::segwit::Witness>> = vec![vec![vec![]]];
512/// # let mut utxo_set = UtxoSet::default();
513/// # let height = 0;
514/// # let mut mempool = Mempool::new();
515/// let ctx = BlockValidationContext::for_network(Network::Regtest);
516/// let (result, new_utxo_set, _) = connect_block(&block, &witnesses, utxo_set, height, &ctx)?;
517/// if matches!(result, ValidationResult::Valid) {
518///     let removed = update_mempool_after_block(&mut mempool, &block, &new_utxo_set)?;
519///     println!("Removed {} transactions from mempool", removed.len());
520/// }
521/// # Ok(())
522/// # }
523/// ```
524#[spec_locked("9.1")]
525pub fn update_mempool_after_block(
526    mempool: &mut Mempool,
527    block: &crate::types::Block,
528    _utxo_set: &crate::types::UtxoSet,
529) -> Result<Vec<Hash>> {
530    let mut removed = Vec::new();
531
532    // 1. Remove transactions that were included in the block
533    for tx in &block.transactions {
534        let tx_id = crate::block::calculate_tx_id(tx);
535        if mempool.remove(&tx_id) {
536            removed.push(tx_id);
537        }
538    }
539
540    // 2. Remove transactions that became invalid (inputs were spent by block)
541    // Note: We don't have the transaction here, just the ID
542    // In a full implementation, we'd need a transaction store or lookup
543    // For now, we'll skip this check and rely on the caller to handle it
544    // Use `update_mempool_after_block_with_lookup` for full validation
545    // This is a limitation that should be addressed with a transaction index
546
547    Ok(removed)
548}
549
550/// Update mempool after block connection (with transaction lookup)
551///
552/// This is a more complete version that can check if mempool transactions
553/// became invalid. Requires a way to look up transactions by ID.
554///
555/// # Arguments
556///
557/// * `mempool` - Mutable reference to the mempool
558/// * `block` - The block that was just connected
559/// * `get_tx_by_id` - Function to look up transactions by ID
560///
561/// # Returns
562///
563/// Returns a vector of transaction IDs that were removed from the mempool.
564#[spec_locked("9.1")]
565pub fn update_mempool_after_block_with_lookup<F>(
566    mempool: &mut Mempool,
567    block: &crate::types::Block,
568    get_tx_by_id: F,
569) -> Result<Vec<Hash>>
570where
571    F: Fn(&Hash) -> Option<crate::types::Transaction>,
572{
573    let mut removed = Vec::new();
574
575    // 1. Remove transactions that were included in the block
576    for tx in &block.transactions {
577        let tx_id = crate::block::calculate_tx_id(tx);
578        if mempool.remove(&tx_id) {
579            removed.push(tx_id);
580        }
581    }
582
583    // 2. Remove transactions that became invalid (inputs were spent by block)
584    // Collect spent outpoints from the block
585    let mut spent_outpoints = std::collections::HashSet::new();
586    for tx in &block.transactions {
587        if !crate::transaction::is_coinbase(tx) {
588            for input in &tx.inputs {
589                spent_outpoints.insert(input.prevout);
590            }
591        }
592    }
593
594    // Check each mempool transaction to see if it spends any of the spent outpoints
595    let mut invalid_tx_ids = Vec::new();
596    for &tx_id in mempool.iter() {
597        if let Some(tx) = get_tx_by_id(&tx_id) {
598            // Check if any input of this transaction was spent by the block
599            for input in &tx.inputs {
600                if spent_outpoints.contains(&input.prevout) {
601                    invalid_tx_ids.push(tx_id);
602                    break;
603                }
604            }
605        }
606    }
607
608    // Remove invalid transactions
609    for tx_id in invalid_tx_ids {
610        if mempool.remove(&tx_id) {
611            removed.push(tx_id);
612        }
613    }
614
615    Ok(removed)
616}
617
618/// Check mempool-specific rules
619fn check_mempool_rules(tx: &Transaction, fee: Integer, mempool: &Mempool) -> Result<bool> {
620    // Check minimum fee rate (simplified)
621    let tx_size = calculate_transaction_size(tx);
622    // Use integer-based fee rate calculation to avoid floating-point precision issues
623    // For display purposes, we still use f64, but for comparisons we use integer math
624    // Runtime assertion: Transaction size must be positive
625    debug_assert!(
626        tx_size > 0,
627        "Transaction size ({tx_size}) must be positive for fee rate calculation"
628    );
629
630    let fee_rate = (fee as f64) / (tx_size as f64);
631
632    // Runtime assertion: Fee rate must be non-negative
633    debug_assert!(
634        fee_rate >= 0.0,
635        "Fee rate ({fee_rate:.6}) must be non-negative (fee: {fee}, size: {tx_size})"
636    );
637
638    // Get minimum fee rate from configuration
639    let config = crate::config::get_consensus_config_ref();
640    let min_fee_rate = config.mempool.min_relay_fee_rate as f64; // sat/vB
641    let min_tx_fee = config.mempool.min_tx_fee; // absolute minimum fee
642
643    // Check absolute minimum fee
644    if fee < min_tx_fee {
645        return Ok(false);
646    }
647
648    // Check fee rate (sat/vB)
649    if fee_rate < min_fee_rate {
650        return Ok(false);
651    }
652
653    // Check mempool size limits using configuration
654    // Use transaction count limit (simpler than size-based for now)
655    if mempool.len() > config.mempool.max_mempool_txs {
656        return Ok(false);
657    }
658
659    Ok(true)
660}
661
662/// Check for transaction conflicts
663fn has_conflicts(tx: &Transaction, mempool: &Mempool) -> Result<bool> {
664    // Check if any input is already spent by mempool transaction
665    for input in &tx.inputs {
666        // In a real implementation, we'd check if this input is already spent
667        // by another transaction in the mempool
668        // For now, we'll do a simplified check
669        if mempool.contains(&input.prevout.hash) {
670            return Ok(true);
671        }
672    }
673
674    Ok(false)
675}
676
677/// Check if transaction is final (Orange Paper Section 9.1 - Transaction Finality)
678///
679/// IsFinalTx — locktime/sequence validation (BIP65/68).
680///
681/// A transaction is final if:
682/// 1. tx.lock_time == 0 (no locktime restriction), OR
683/// 2. If locktime < LOCKTIME_THRESHOLD (block height): height > tx.lock_time
684/// 3. If locktime >= LOCKTIME_THRESHOLD (timestamp): block_time > tx.lock_time
685/// 4. OR if all inputs have SEQUENCE_FINAL (0xffffffff), locktime is ignored
686///
687/// Mathematical specification:
688/// ∀ tx ∈ Transaction, height ∈ ℕ, block_time ∈ ℕ:
689/// - is_final_tx(tx, height, block_time) = true ⟹
690///   (tx.lock_time = 0 ∨
691///   (tx.lock_time < LOCKTIME_THRESHOLD ∧ height > tx.lock_time) ∨
692///   (tx.lock_time >= LOCKTIME_THRESHOLD ∧ block_time > tx.lock_time) ∨
693///   (∀ input ∈ tx.inputs: input.sequence == SEQUENCE_FINAL))
694///
695/// Check if transaction is final (Orange Paper Section 9.1 - Transaction Finality)
696///
697/// IsFinalTx — locktime/sequence validation (BIP65/68).
698///
699/// A transaction is final if:
700/// 1. tx.lock_time == 0 (no locktime restriction), OR
701/// 2. If locktime < LOCKTIME_THRESHOLD (block height): height > tx.lock_time
702/// 3. If locktime >= LOCKTIME_THRESHOLD (timestamp): block_time > tx.lock_time
703/// 4. OR if all inputs have SEQUENCE_FINAL (0xffffffff), locktime is ignored
704///
705/// Mathematical specification:
706/// ∀ tx ∈ Transaction, height ∈ ℕ, block_time ∈ ℕ:
707/// - is_final_tx(tx, height, block_time) = true ⟹
708///   (tx.lock_time = 0 ∨
709///   (tx.lock_time < LOCKTIME_THRESHOLD ∧ height > tx.lock_time) ∨
710///   (tx.lock_time >= LOCKTIME_THRESHOLD ∧ block_time > tx.lock_time) ∨
711///   (∀ input ∈ tx.inputs: input.sequence == SEQUENCE_FINAL))
712///
713/// # Arguments
714/// * `tx` - Transaction to check
715/// * `height` - Current block height
716/// * `block_time` - Median time-past of chain tip (BIP113) for timestamp locktime validation
717#[spec_locked("9.1")]
718pub fn is_final_tx(tx: &Transaction, height: Natural, block_time: Natural) -> bool {
719    use crate::constants::SEQUENCE_FINAL;
720
721    // If locktime is 0, transaction is always final
722    if tx.lock_time == 0 {
723        return true;
724    }
725
726    // Check if locktime is satisfied based on type
727    // If locktime < threshold, compare to block height; else compare to block time
728    // This means: locktime < (condition ? height : block_time)
729    // So: if locktime < threshold, check locktime < height
730    //     if locktime >= threshold, check locktime < block_time
731    let locktime_satisfied = if (tx.lock_time as u32) < LOCKTIME_THRESHOLD {
732        // Block height locktime: check if locktime < height
733        (tx.lock_time as Natural) < height
734    } else {
735        // Timestamp locktime: check if locktime < block_time
736        (tx.lock_time as Natural) < block_time
737    };
738
739    if locktime_satisfied {
740        return true;
741    }
742
743    // Even if locktime isn't satisfied, transaction is final if all inputs have SEQUENCE_FINAL
744    // This allows transactions to bypass locktime by setting all sequences to 0xffffffff
745    // If all inputs have SEQUENCE_FINAL, locktime is ignored
746    for input in &tx.inputs {
747        if (input.sequence as u32) != SEQUENCE_FINAL {
748            return false;
749        }
750    }
751
752    // All inputs have SEQUENCE_FINAL - transaction is final regardless of locktime
753    true
754}
755
756/// Check if transaction signals RBF
757///
758/// Returns true if any input has nSequence < SEQUENCE_FINAL (0xffffffff)
759#[spec_locked("9.3")]
760pub fn signals_rbf(tx: &Transaction) -> bool {
761    for input in &tx.inputs {
762        if (input.sequence as u32) < SEQUENCE_FINAL {
763            return true;
764        }
765    }
766    false
767}
768
769/// Calculate transaction size in virtual bytes (vbytes)
770///
771/// For SegWit transactions, uses weight/4 (virtual bytes).
772/// For non-SegWit transactions, uses byte size.
773/// This is a simplified version - proper implementation would use segwit::calculate_weight()
774fn calculate_transaction_size_vbytes(tx: &Transaction) -> usize {
775    // Simplified: use byte size as approximation
776    // In production, should use proper weight calculation for SegWit
777    calculate_transaction_size(tx)
778}
779
780/// Check if new transaction conflicts with existing transaction
781///
782/// A conflict exists if new_tx spends at least one input from existing_tx.
783/// This is requirement #4 of BIP125.
784#[spec_locked("9.3")]
785pub fn has_conflict_with_tx(new_tx: &Transaction, existing_tx: &Transaction) -> bool {
786    for new_input in &new_tx.inputs {
787        for existing_input in &existing_tx.inputs {
788            if new_input.prevout == existing_input.prevout {
789                return true;
790            }
791        }
792    }
793    false
794}
795
796/// Check if new transaction creates new unconfirmed dependencies
797///
798/// BIP125 requirement #5: All inputs of tx_2 must be:
799/// - Confirmed (in UTXO set), OR
800/// - From tx_1 (spending the same inputs)
801fn creates_new_dependencies(
802    new_tx: &Transaction,
803    existing_tx: &Transaction,
804    utxo_set: &UtxoSet,
805    mempool: &Mempool,
806) -> Result<bool> {
807    for input in &new_tx.inputs {
808        // Check if input is confirmed (in UTXO set)
809        if utxo_set.contains_key(&input.prevout) {
810            continue;
811        }
812
813        // Check if input was spent by existing transaction
814        let mut found_in_existing = false;
815        for existing_input in &existing_tx.inputs {
816            if existing_input.prevout == input.prevout {
817                found_in_existing = true;
818                break;
819            }
820        }
821
822        if found_in_existing {
823            continue;
824        }
825
826        // If not confirmed and not from existing tx, it's a new unconfirmed dependency
827        // Check if it's at least in mempool (but still unconfirmed)
828        if !mempool.contains(&input.prevout.hash) {
829            return Ok(true); // New unconfirmed dependency
830        }
831    }
832
833    Ok(false)
834}
835
836/// Check if script is standard
837fn is_standard_script(script: &ByteString) -> Result<bool> {
838    // Simplified standard script check
839    // In reality, this would check for P2PKH, P2SH, P2WPKH, P2WSH, etc.
840    if script.is_empty() {
841        return Ok(false);
842    }
843
844    // Basic checks
845    if script.len() > MAX_SCRIPT_SIZE {
846        return Ok(false);
847    }
848
849    // Check for non-standard opcodes (simplified)
850    for &byte in script {
851        if byte > 0x60 && byte < 0x7f {
852            // Some non-standard opcodes
853            return Ok(false);
854        }
855    }
856
857    Ok(true)
858}
859
860/// Calculate transaction ID (deprecated - use crate::block::calculate_tx_id instead)
861///
862/// This function is kept for backward compatibility but delegates to the
863/// standard implementation in block.rs.
864#[deprecated(note = "Use crate::block::calculate_tx_id instead")]
865#[spec_locked("5.1")]
866pub fn calculate_tx_id(tx: &Transaction) -> Hash {
867    crate::block::calculate_tx_id(tx)
868}
869
870/// Calculate transaction size (simplified)
871// Use the actual serialization-based size calculation from transaction module
872// Ensures consistency with base serialization size (no witness)
873fn calculate_transaction_size(tx: &Transaction) -> usize {
874    use crate::transaction::calculate_transaction_size as tx_size;
875    tx_size(tx)
876}
877
878/// Check if transaction is coinbase
879fn is_coinbase(tx: &Transaction) -> bool {
880    // Optimization: Use constant folding for zero hash check
881    #[cfg(feature = "production")]
882    {
883        use crate::optimizations::constant_folding::is_zero_hash;
884        tx.inputs.len() == 1
885            && is_zero_hash(&tx.inputs[0].prevout.hash)
886            && tx.inputs[0].prevout.index == 0xffffffff
887    }
888
889    #[cfg(not(feature = "production"))]
890    {
891        tx.inputs.len() == 1
892            && tx.inputs[0].prevout.hash == [0u8; 32]
893            && tx.inputs[0].prevout.index == 0xffffffff
894    }
895}
896
897// ============================================================================
898// FORMAL VERIFICATION
899// ============================================================================
900
901/// Mathematical Specification for Mempool:
902/// ∀ tx ∈ 𝒯𝒳, utxo_set ∈ 𝒰𝒮, mempool ∈ Mempool:
903/// - accept_to_memory_pool(tx, utxo_set, mempool) = Accepted ⟹
904///   (tx ∉ mempool ∧
905///    CheckTransaction(tx) = valid ∧
906///    CheckTxInputs(tx, utxo_set) = valid ∧
907///    VerifyScripts(tx) = valid ∧
908///    ¬has_conflicts(tx, mempool))
909///
910/// Invariants:
911/// - Mempool never contains duplicate transactions
912/// - Mempool never contains conflicting transactions
913/// - Accepted transactions are valid
914/// - RBF rules are enforced
915
916#[cfg(test)]
917mod tests {
918    use super::*;
919    use crate::opcodes::*;
920
921    #[test]
922    fn test_accept_to_memory_pool_valid() {
923        // Skip script validation for now - focus on mempool logic
924        let tx = create_valid_transaction();
925        let utxo_set = create_test_utxo_set();
926        let mempool = Mempool::new();
927
928        // This will fail on script validation, but that's expected
929        let time_context = Some(TimeContext {
930            network_time: 1234567890,
931            median_time_past: 1234567890,
932        });
933        let result =
934            accept_to_memory_pool(&tx, None, &utxo_set, &mempool, 100, time_context).unwrap();
935        assert!(matches!(result, MempoolResult::Rejected(_)));
936    }
937
938    #[test]
939    fn test_accept_to_memory_pool_duplicate() {
940        let tx = create_valid_transaction();
941        let utxo_set = create_test_utxo_set();
942        let mut mempool = Mempool::new();
943        mempool.insert(crate::block::calculate_tx_id(&tx));
944
945        let time_context = Some(TimeContext {
946            network_time: 1234567890,
947            median_time_past: 1234567890,
948        });
949        let result =
950            accept_to_memory_pool(&tx, None, &utxo_set, &mempool, 100, time_context).unwrap();
951        assert!(matches!(result, MempoolResult::Rejected(_)));
952    }
953
954    #[test]
955    fn test_is_standard_tx_valid() {
956        let tx = create_valid_transaction();
957        assert!(is_standard_tx(&tx).unwrap());
958    }
959
960    #[test]
961    fn test_is_standard_tx_too_large() {
962        let mut tx = create_valid_transaction();
963        // Make transaction too large by adding many inputs
964        // MAX_INPUTS (100,000) * ~42 bytes/input >> MAX_TX_SIZE (1,000,000)
965        for _ in 0..MAX_INPUTS {
966            tx.inputs.push(create_dummy_input());
967        }
968        // Transaction exceeds MAX_TX_SIZE so it should NOT be standard
969        assert!(!is_standard_tx(&tx).unwrap());
970    }
971
972    #[test]
973    fn test_replacement_checks_all_requirements() {
974        let utxo_set = create_test_utxo_set();
975        let mempool = Mempool::new();
976
977        // Create existing transaction with RBF signaling and lower fee
978        let mut existing_tx = create_valid_transaction();
979        existing_tx.inputs[0].sequence = SEQUENCE_RBF as u64;
980        existing_tx.outputs[0].value = 9000; // Fee = 10000 - 9000 = 1000 sats
981
982        // Create new transaction that:
983        // 1. Signals RBF (or doesn't - per BIP125 only existing needs to signal)
984        // 2. Conflicts with existing (same input)
985        // 3. Has higher fee rate and absolute fee
986        let mut new_tx = existing_tx.clone();
987        new_tx.outputs[0].value = 8000; // Fee = 10000 - 8000 = 2000 sats
988                                        // Higher fee rate and absolute fee bump (2000 > 1000 + 1000 = 2000, needs >)
989        new_tx.outputs[0].value = 7999; // Fee = 10000 - 7999 = 2001 sats
990
991        // Should pass all BIP125 checks
992        let result = replacement_checks(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap();
993        assert!(result, "Valid RBF replacement should be accepted");
994    }
995
996    #[test]
997    fn test_replacement_checks_no_rbf_signal() {
998        let utxo_set = create_test_utxo_set();
999        let mempool = Mempool::new();
1000
1001        let new_tx = create_valid_transaction();
1002        let existing_tx = create_valid_transaction(); // No RBF signal
1003
1004        // Should fail: existing transaction doesn't signal RBF
1005        assert!(!replacement_checks(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap());
1006    }
1007
1008    #[test]
1009    fn test_replacement_checks_no_conflict() {
1010        let mut utxo_set = create_test_utxo_set();
1011        // Add UTXO for the new transaction's input
1012        let new_outpoint = OutPoint {
1013            hash: [2; 32],
1014            index: 0,
1015        };
1016        let new_utxo = UTXO {
1017            value: 10000,
1018            script_pubkey: vec![OP_1].into(),
1019            height: 0,
1020            is_coinbase: false,
1021        };
1022        utxo_set.insert(new_outpoint, std::sync::Arc::new(new_utxo));
1023
1024        let mempool = Mempool::new();
1025
1026        let mut existing_tx = create_valid_transaction();
1027        existing_tx.inputs[0].sequence = SEQUENCE_RBF as u64;
1028
1029        // New transaction with different input (no conflict)
1030        let mut new_tx = create_valid_transaction();
1031        new_tx.inputs[0].prevout.hash = [2; 32]; // Different input
1032        new_tx.inputs[0].sequence = SEQUENCE_RBF as u64;
1033        // Ensure output value doesn't exceed input value to avoid negative fee
1034        new_tx.outputs[0].value = 5000; // Less than input value of 10000
1035
1036        // Should fail: no conflict (requirement #4)
1037        assert!(!replacement_checks(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap());
1038    }
1039
1040    #[test]
1041    fn test_replacement_checks_fee_rate_too_low() {
1042        let utxo_set = create_test_utxo_set();
1043        let mempool = Mempool::new();
1044
1045        // Existing transaction with higher fee rate
1046        let mut existing_tx = create_valid_transaction();
1047        existing_tx.inputs[0].sequence = SEQUENCE_RBF as u64;
1048        existing_tx.outputs[0].value = 5000; // Fee = 5000 sats, size = small
1049
1050        // New transaction with same or lower fee rate (but higher absolute fee)
1051        let mut new_tx = existing_tx.clone();
1052        new_tx.outputs[0].value = 4999; // Fee = 5001 sats, but same size so same fee rate
1053
1054        // Should fail: fee rate not higher (requirement #2)
1055        assert!(!replacement_checks(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap());
1056    }
1057
1058    #[test]
1059    fn test_replacement_checks_absolute_fee_insufficient() {
1060        let utxo_set = create_test_utxo_set();
1061        let mempool = Mempool::new();
1062
1063        // Existing transaction
1064        let mut existing_tx = create_valid_transaction();
1065        existing_tx.inputs[0].sequence = SEQUENCE_RBF as u64;
1066        existing_tx.outputs[0].value = 9000; // Fee = 1000 sats
1067
1068        // New transaction with higher fee rate but insufficient absolute fee bump
1069        // Fee must be > 1000 + 1000 = 2000, so need > 2000
1070        let mut new_tx = existing_tx.clone();
1071        new_tx.outputs[0].value = 8001; // Fee = 1999 sats (insufficient)
1072
1073        // Should fail: absolute fee not high enough (requirement #3)
1074        assert!(!replacement_checks(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap());
1075
1076        // Now with sufficient fee
1077        new_tx.outputs[0].value = 7999; // Fee = 2001 sats (sufficient)
1078                                        // Should still fail on other checks (conflict, etc.), but fee check passes
1079                                        // For full test, need to ensure conflict exists
1080    }
1081
1082    // ============================================================================
1083    // COMPREHENSIVE MEMPOOL TESTS
1084    // ============================================================================
1085
1086    #[test]
1087    fn test_accept_to_memory_pool_coinbase() {
1088        let coinbase_tx = create_coinbase_transaction();
1089        let utxo_set = UtxoSet::default();
1090        let mempool = Mempool::new();
1091        // Coinbase transactions should be rejected from mempool
1092        let time_context = Some(TimeContext {
1093            network_time: 0,
1094            median_time_past: 0,
1095        });
1096        let result =
1097            accept_to_memory_pool(&coinbase_tx, None, &utxo_set, &mempool, 100, time_context)
1098                .unwrap();
1099        assert!(matches!(result, MempoolResult::Rejected(_)));
1100    }
1101
1102    #[test]
1103    fn test_is_standard_tx_large_script() {
1104        let mut tx = create_valid_transaction();
1105        // Create a script that's too large
1106        tx.inputs[0].script_sig = vec![OP_1; MAX_SCRIPT_SIZE + 1];
1107
1108        let result = is_standard_tx(&tx).unwrap();
1109        assert!(!result);
1110    }
1111
1112    #[test]
1113    fn test_is_standard_tx_large_output_script() {
1114        let mut tx = create_valid_transaction();
1115        // Create an output script that's too large
1116        tx.outputs[0].script_pubkey = vec![OP_1; MAX_SCRIPT_SIZE + 1];
1117
1118        let result = is_standard_tx(&tx).unwrap();
1119        assert!(!result);
1120    }
1121
1122    #[test]
1123    fn test_replacement_checks_new_unconfirmed_dependency() {
1124        let utxo_set = create_test_utxo_set();
1125        let mempool = Mempool::new();
1126
1127        // Existing transaction
1128        let mut existing_tx = create_valid_transaction();
1129        existing_tx.inputs[0].sequence = SEQUENCE_RBF as u64;
1130
1131        // New transaction that adds a new unconfirmed input
1132        let mut new_tx = existing_tx.clone();
1133        new_tx.inputs.push(TransactionInput {
1134            prevout: OutPoint {
1135                hash: [99; 32],
1136                index: 0,
1137            }, // Not in UTXO set
1138            script_sig: vec![],
1139            sequence: SEQUENCE_RBF as u64,
1140        });
1141        new_tx.outputs[0].value = 7000; // Higher fee
1142
1143        // Should fail: creates new unconfirmed dependency (requirement #5)
1144        assert!(!replacement_checks(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap());
1145    }
1146
1147    #[test]
1148    fn test_has_conflict_with_tx_true() {
1149        let tx1 = create_valid_transaction();
1150        let mut tx2 = create_valid_transaction();
1151        tx2.inputs[0].prevout = tx1.inputs[0].prevout.clone(); // Same input = conflict
1152
1153        assert!(has_conflict_with_tx(&tx2, &tx1));
1154    }
1155
1156    #[test]
1157    fn test_has_conflict_with_tx_false() {
1158        let tx1 = create_valid_transaction();
1159        let mut tx2 = create_valid_transaction();
1160        tx2.inputs[0].prevout.hash = [2; 32]; // Different input = no conflict
1161
1162        assert!(!has_conflict_with_tx(&tx2, &tx1));
1163    }
1164
1165    #[test]
1166    fn test_replacement_checks_minimum_relay_fee() {
1167        let utxo_set = create_test_utxo_set();
1168        let mempool = Mempool::new();
1169
1170        // Existing transaction
1171        let mut existing_tx = create_valid_transaction();
1172        existing_tx.inputs[0].sequence = SEQUENCE_RBF as u64;
1173        existing_tx.outputs[0].value = 9500; // Fee = 500 sats
1174
1175        // New transaction with exactly MIN_RELAY_FEE bump (not enough, need >)
1176        let mut new_tx = existing_tx.clone();
1177        new_tx.outputs[0].value = 8500; // Fee = 1500 sats (1500 > 500 + 1000 = 1500? No, need >)
1178        assert!(!replacement_checks(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap());
1179
1180        // New transaction with sufficient bump
1181        // Fee = 1501 sats (1501 > 500 + 1000 = 1500)
1182        // Conflict detection and fee rate validation are handled by accept_to_memory_pool
1183        new_tx.outputs[0].value = 8499;
1184    }
1185
1186    #[test]
1187    fn test_check_mempool_rules_low_fee() {
1188        let tx = create_valid_transaction();
1189        let fee = 1; // Very low fee
1190        let mempool = Mempool::new();
1191
1192        let result = check_mempool_rules(&tx, fee, &mempool).unwrap();
1193        assert!(!result);
1194    }
1195
1196    #[test]
1197    fn test_check_mempool_rules_high_fee() {
1198        let tx = create_valid_transaction();
1199        let fee = 10000; // High fee
1200        let mempool = Mempool::new();
1201
1202        let result = check_mempool_rules(&tx, fee, &mempool).unwrap();
1203        assert!(result);
1204    }
1205
1206    #[test]
1207    fn test_check_mempool_rules_full_mempool() {
1208        let tx = create_valid_transaction();
1209        let fee = 10000;
1210        let mut mempool = Mempool::new();
1211
1212        // Fill mempool beyond limit with unique hashes
1213        // Default max_mempool_txs is 100,000, so we need to exceed that
1214        for i in 0..100_001 {
1215            let mut hash = [0u8; 32];
1216            hash[0] = (i & 0xff) as u8;
1217            hash[1] = ((i >> 8) & 0xff) as u8;
1218            hash[2] = ((i >> 16) & 0xff) as u8;
1219            hash[3] = ((i >> 24) & 0xff) as u8;
1220            mempool.insert(hash);
1221        }
1222
1223        // Verify mempool is actually full (exceeds max_mempool_txs limit of 100,000)
1224        assert!(mempool.len() > 100_000);
1225
1226        let result = check_mempool_rules(&tx, fee, &mempool).unwrap();
1227        assert!(!result);
1228    }
1229
1230    #[test]
1231    fn test_has_conflicts_no_conflicts() {
1232        let tx = create_valid_transaction();
1233        let mempool = Mempool::new();
1234
1235        let result = has_conflicts(&tx, &mempool).unwrap();
1236        assert!(!result);
1237    }
1238
1239    #[test]
1240    fn test_has_conflicts_with_conflicts() {
1241        let tx = create_valid_transaction();
1242        let mut mempool = Mempool::new();
1243
1244        // Add a conflicting transaction to mempool
1245        mempool.insert(tx.inputs[0].prevout.hash);
1246
1247        let result = has_conflicts(&tx, &mempool).unwrap();
1248        assert!(result);
1249    }
1250
1251    #[test]
1252    fn test_signals_rbf_true() {
1253        let mut tx = create_valid_transaction();
1254        tx.inputs[0].sequence = 0xfffffffe; // RBF signal
1255
1256        assert!(signals_rbf(&tx));
1257    }
1258
1259    #[test]
1260    fn test_signals_rbf_false() {
1261        let tx = create_valid_transaction(); // sequence = 0xffffffff (final)
1262
1263        assert!(!signals_rbf(&tx));
1264    }
1265
1266    #[test]
1267    fn test_calculate_fee_rate() {
1268        let tx = create_valid_transaction();
1269        let utxo_set = create_test_utxo_set();
1270        let fee = calculate_fee(&tx, &utxo_set);
1271
1272        // Fee should be calculable (may be 0 for valid transactions)
1273        assert!(fee.is_ok());
1274    }
1275
1276    #[test]
1277    fn test_creates_new_dependencies_no_new() {
1278        let new_tx = create_valid_transaction();
1279        let existing_tx = create_valid_transaction();
1280        let mempool = Mempool::new();
1281
1282        let utxo_set = create_test_utxo_set();
1283        let result = creates_new_dependencies(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap();
1284        assert!(!result);
1285    }
1286
1287    #[test]
1288    fn test_creates_new_dependencies_with_new() {
1289        let mut new_tx = create_valid_transaction();
1290        let existing_tx = create_valid_transaction();
1291        let mempool = Mempool::new();
1292
1293        // Make new_tx spend a different input
1294        new_tx.inputs[0].prevout.hash = [2; 32];
1295
1296        let utxo_set = create_test_utxo_set();
1297        let result = creates_new_dependencies(&new_tx, &existing_tx, &utxo_set, &mempool).unwrap();
1298        assert!(result);
1299    }
1300
1301    #[test]
1302    fn test_is_standard_script_empty() {
1303        let script = vec![];
1304        let result = is_standard_script(&script).unwrap();
1305        assert!(!result);
1306    }
1307
1308    #[test]
1309    fn test_is_standard_script_too_large() {
1310        let script = vec![OP_1; MAX_SCRIPT_SIZE + 1];
1311        let result = is_standard_script(&script).unwrap();
1312        assert!(!result);
1313    }
1314
1315    #[test]
1316    fn test_is_standard_script_non_standard_opcode() {
1317        let script = vec![OP_VERIF]; // Non-standard opcode (disabled)
1318        let result = is_standard_script(&script).unwrap();
1319        assert!(!result);
1320    }
1321
1322    #[test]
1323    fn test_is_standard_script_valid() {
1324        let script = vec![OP_1];
1325        let result = is_standard_script(&script).unwrap();
1326        assert!(result);
1327    }
1328
1329    #[test]
1330    fn test_calculate_tx_id() {
1331        let tx = create_valid_transaction();
1332        let tx_id = crate::block::calculate_tx_id(&tx);
1333
1334        // Should be a 32-byte hash
1335        assert_eq!(tx_id.len(), 32);
1336
1337        // Same transaction should produce same ID
1338        let tx_id2 = crate::block::calculate_tx_id(&tx);
1339        assert_eq!(tx_id, tx_id2);
1340    }
1341
1342    #[test]
1343    fn test_calculate_tx_id_different_txs() {
1344        let tx1 = create_valid_transaction();
1345        let mut tx2 = tx1.clone();
1346        tx2.version = 2; // Different version
1347
1348        let id1 = crate::block::calculate_tx_id(&tx1);
1349        let id2 = crate::block::calculate_tx_id(&tx2);
1350
1351        assert_ne!(id1, id2);
1352    }
1353
1354    #[test]
1355    fn test_calculate_transaction_size() {
1356        let tx = create_valid_transaction();
1357        let size = calculate_transaction_size(&tx);
1358
1359        assert!(size > 0);
1360
1361        // Size should be deterministic
1362        let size2 = calculate_transaction_size(&tx);
1363        assert_eq!(size, size2);
1364    }
1365
1366    #[test]
1367    fn test_calculate_transaction_size_multiple_inputs_outputs() {
1368        let mut tx = create_valid_transaction();
1369        tx.inputs.push(create_dummy_input());
1370        tx.outputs.push(create_dummy_output());
1371
1372        let size = calculate_transaction_size(&tx);
1373        assert!(size > 0);
1374    }
1375
1376    #[test]
1377    fn test_is_coinbase_true() {
1378        let coinbase_tx = create_coinbase_transaction();
1379        assert!(is_coinbase(&coinbase_tx));
1380    }
1381
1382    #[test]
1383    fn test_is_coinbase_false() {
1384        let regular_tx = create_valid_transaction();
1385        assert!(!is_coinbase(&regular_tx));
1386    }
1387
1388    // Helper functions for tests
1389    fn create_valid_transaction() -> Transaction {
1390        Transaction {
1391            version: 1,
1392            inputs: vec![create_dummy_input()].into(),
1393            outputs: vec![create_dummy_output()].into(),
1394            lock_time: 0,
1395        }
1396    }
1397
1398    fn create_dummy_input() -> TransactionInput {
1399        TransactionInput {
1400            prevout: OutPoint {
1401                hash: [1; 32],
1402                index: 0,
1403            },
1404            script_sig: vec![OP_1],
1405            sequence: 0xffffffff,
1406        }
1407    }
1408
1409    fn create_dummy_output() -> TransactionOutput {
1410        TransactionOutput {
1411            value: 1000,
1412            script_pubkey: vec![OP_1].into(), // OP_1 for valid script
1413        }
1414    }
1415
1416    fn create_test_utxo_set() -> UtxoSet {
1417        let mut utxo_set = UtxoSet::default();
1418        let outpoint = OutPoint {
1419            hash: [1; 32],
1420            index: 0,
1421        };
1422        let utxo = UTXO {
1423            value: 10000,
1424            script_pubkey: vec![OP_1].into(), // OP_1 for valid script
1425            height: 0,
1426            is_coinbase: false,
1427        };
1428        utxo_set.insert(outpoint, std::sync::Arc::new(utxo));
1429        utxo_set
1430    }
1431
1432    fn create_coinbase_transaction() -> Transaction {
1433        Transaction {
1434            version: 1,
1435            inputs: vec![TransactionInput {
1436                prevout: OutPoint {
1437                    hash: [0; 32].into(),
1438                    index: 0xffffffff,
1439                },
1440                script_sig: vec![],
1441                sequence: 0xffffffff,
1442            }]
1443            .into(),
1444            outputs: vec![TransactionOutput {
1445                value: 5000000000,
1446                script_pubkey: vec![].into(),
1447            }]
1448            .into(),
1449            lock_time: 0,
1450        }
1451    }
1452}