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