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