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