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