Skip to main content

blvm_consensus/
mempool.rs

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