Skip to content

Commit 97c1bfd

Browse files
committed
test: add test for standardness and mempool policy checks
1 parent 50e6be8 commit 97c1bfd

5 files changed

Lines changed: 581 additions & 0 deletions

File tree

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ mod rbf;
1919
mod selection;
2020
mod selector;
2121
mod signer;
22+
#[cfg(test)]
23+
pub(crate) mod test_utils;
2224
mod utils;
2325

2426
pub use canonical_unspents::*;

src/policy.rs

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,3 +380,258 @@ impl core::fmt::Display for MempoolPolicyError {
380380

381381
#[cfg(feature = "std")]
382382
impl std::error::Error for MempoolPolicyError {}
383+
384+
#[cfg(test)]
385+
mod tests {
386+
use super::*;
387+
use crate::{test_utils::*, Output};
388+
use alloc::vec::Vec;
389+
use bitcoin::{transaction::Version, Amount, ScriptBuf, Transaction, TxOut};
390+
391+
fn default_tip() -> ChainTip {
392+
ChainTip {
393+
height: absolute::Height::from_consensus(3_000).unwrap(),
394+
mtp: absolute::Time::from_consensus(500_001_000).unwrap(),
395+
}
396+
}
397+
398+
#[test]
399+
fn default_policy_matches_bitcoin_core_v30() {
400+
let default_policy = MempoolPolicy::default();
401+
let v30 = MempoolPolicy::bitcoin_core_v30();
402+
assert_eq!(default_policy.dust_relay_feerate, v30.dust_relay_feerate);
403+
assert_eq!(default_policy.min_relay_feerate, v30.min_relay_feerate);
404+
assert_eq!(
405+
default_policy.max_op_return_aggregate_bytes,
406+
v30.max_op_return_aggregate_bytes
407+
);
408+
assert_eq!(
409+
default_policy.max_standard_tx_weight,
410+
v30.max_standard_tx_weight
411+
);
412+
assert_eq!(default_policy.max_truc_tx_weight, v30.max_truc_tx_weight);
413+
assert_eq!(
414+
default_policy.min_standard_tx_nonwitness_size,
415+
v30.min_standard_tx_nonwitness_size
416+
);
417+
assert_eq!(
418+
default_policy.max_witness_stack_items,
419+
v30.max_witness_stack_items
420+
);
421+
assert_eq!(default_policy.allowed_versions, v30.allowed_versions);
422+
}
423+
424+
#[test]
425+
fn test_tx_version() {
426+
let policy = MempoolPolicy::default();
427+
let tip = default_tip();
428+
let input = setup_test_input(2_000).unwrap();
429+
let output = create_output(p2tr_script(), 9_000);
430+
let (selection, mut tx) = build_selection_with_tx(&[input], &[output]);
431+
432+
// Default version is 2, which is standard.
433+
assert!(policy.check_post_selection(&selection, &tx, tip).is_ok());
434+
435+
// Test version 1, which is also standard.
436+
tx.version = Version::ONE;
437+
assert!(policy.check_post_selection(&selection, &tx, tip).is_ok());
438+
439+
// Version 3 (TRUC) is standard under v30+.
440+
tx.version = Version(3);
441+
assert!(policy.check_post_selection(&selection, &tx, tip).is_ok());
442+
443+
// Test version 4, which is non-standard.
444+
tx.version = Version(4);
445+
assert!(matches!(
446+
policy.check_post_selection(&selection, &tx, tip),
447+
Err(MempoolPolicyError::UnsupportedVersion(_))
448+
));
449+
}
450+
451+
#[test]
452+
fn test_tx_locktime() {
453+
let policy = MempoolPolicy::default();
454+
let tip = default_tip();
455+
let input = setup_test_input(2_000).unwrap();
456+
let output = create_output(p2tr_script(), 9_000);
457+
let (selection, mut tx) = build_selection_with_tx(&[input], &[output]);
458+
459+
// Locktime exactly equal to the tip height.
460+
tx.lock_time = absolute::LockTime::from_consensus(3_000);
461+
assert!(policy.check_post_selection(&selection, &tx, tip).is_ok());
462+
463+
// Locktime below the tip height.
464+
tx.lock_time = absolute::LockTime::from_consensus(2_500);
465+
assert!(policy.check_post_selection(&selection, &tx, tip).is_ok());
466+
467+
// Locktime above the tip height.
468+
tx.lock_time = absolute::LockTime::from_consensus(3_001);
469+
assert!(matches!(
470+
policy.check_post_selection(&selection, &tx, tip),
471+
Err(MempoolPolicyError::LockTimeNotMet(_))
472+
));
473+
474+
// Locktime one second below the tip MTP.
475+
tx.lock_time = absolute::LockTime::from_consensus(500_000_999);
476+
assert!(policy.check_post_selection(&selection, &tx, tip).is_ok());
477+
478+
// Locktime exactly equal to the tip MTP.
479+
tx.lock_time = absolute::LockTime::from_consensus(500_001_000);
480+
assert!(matches!(
481+
policy.check_post_selection(&selection, &tx, tip),
482+
Err(MempoolPolicyError::LockTimeNotMet(_))
483+
));
484+
485+
// Locktime above the tip MTP.
486+
tx.lock_time = absolute::LockTime::from_consensus(500_002_000);
487+
assert!(matches!(
488+
policy.check_post_selection(&selection, &tx, tip),
489+
Err(MempoolPolicyError::LockTimeNotMet(_))
490+
));
491+
}
492+
493+
#[test]
494+
fn test_max_tx_weight() {
495+
let policy = MempoolPolicy::default();
496+
let tip = default_tip();
497+
498+
// A normal transaction with 1 input and 1 output.
499+
let input = setup_test_input(2_000).unwrap();
500+
let output = create_output(p2tr_script(), 9_000);
501+
let (selection, tx) = build_selection_with_tx(core::slice::from_ref(&input), &[output]);
502+
assert!(policy.check_post_selection(&selection, &tx, tip).is_ok());
503+
504+
// Heavy transaction with excess weight.
505+
let outputs_with_excess_weight: Vec<Output> = (0..2_350)
506+
.map(|_| create_output(p2tr_script(), 1_000))
507+
.collect();
508+
509+
let (_, heavy_tx) =
510+
build_selection_with_tx(&[input], outputs_with_excess_weight.as_slice());
511+
512+
assert!(heavy_tx.weight() > policy.max_standard_tx_weight);
513+
assert!(matches!(
514+
policy.check_max_tx_weight(heavy_tx.weight(), heavy_tx.version),
515+
Err(MempoolPolicyError::MaxWeightExceeded { .. })
516+
));
517+
}
518+
519+
#[test]
520+
fn test_tx_min_non_witness_size() {
521+
let policy = MempoolPolicy::default();
522+
let tip = default_tip();
523+
let input = setup_test_input(2_000).unwrap();
524+
let output = create_output(p2tr_script(), 9_000);
525+
526+
// Transaction with 1 input and 1 output.
527+
let (selection, tx) = build_selection_with_tx(&[input], &[output]);
528+
assert!(policy.check_post_selection(&selection, &tx, tip).is_ok());
529+
530+
// Transaction with no inputs and 1 output.
531+
let tx_below_min_non_witness_size = Transaction {
532+
version: Version::TWO,
533+
lock_time: absolute::LockTime::ZERO,
534+
input: vec![],
535+
output: vec![TxOut {
536+
script_pubkey: ScriptBuf::new(),
537+
value: Amount::ZERO,
538+
}],
539+
};
540+
let empty_selection = Selection {
541+
inputs: vec![],
542+
outputs: vec![Output::with_script(ScriptBuf::new(), Amount::ZERO)],
543+
};
544+
assert!(tx_below_min_non_witness_size.base_size() < policy.min_standard_tx_nonwitness_size);
545+
assert!(matches!(
546+
policy.check_post_selection(&empty_selection, &tx_below_min_non_witness_size, tip),
547+
Err(MempoolPolicyError::TxTooSmall { .. })
548+
));
549+
}
550+
551+
#[test]
552+
fn test_min_fee_relay() {
553+
let policy = MempoolPolicy::default();
554+
let tip = default_tip();
555+
556+
// Sufficient fee passes.
557+
let input = setup_test_input(2_000).unwrap();
558+
let output = create_output(p2tr_script(), 9_000);
559+
560+
let (selection, tx) = build_selection_with_tx(&[input], &[output]);
561+
assert!(policy.check_post_selection(&selection, &tx, tip).is_ok());
562+
563+
// Fee below the 1 sat/vB minimum is rejected.
564+
let input_with_insufficient_fee = setup_test_input(2_000).unwrap();
565+
let output_with_insufficient_fee = create_output(p2tr_script(), 9_999);
566+
567+
let (selection_with_insufficient_fee, tx_with_insufficient_fee) = build_selection_with_tx(
568+
&[input_with_insufficient_fee],
569+
&[output_with_insufficient_fee],
570+
);
571+
assert!(matches!(
572+
policy.check_post_selection(
573+
&selection_with_insufficient_fee,
574+
&tx_with_insufficient_fee,
575+
tip
576+
),
577+
Err(MempoolPolicyError::MinRelayFeeNotMet { .. })
578+
));
579+
}
580+
581+
#[test]
582+
fn test_max_witness_stack() {
583+
let policy = MempoolPolicy::default();
584+
let input = setup_test_input(2_000).unwrap();
585+
586+
assert!(policy.check_max_witness_stack(&[input]).is_ok());
587+
}
588+
589+
#[test]
590+
fn test_input_spendability() {
591+
let policy = MempoolPolicy::default();
592+
let tip = default_tip();
593+
594+
// Confirmed input.
595+
let input = setup_test_input(2_000).unwrap();
596+
assert!(policy.check_input_spendability(&[input], tip).is_ok());
597+
598+
// Immature coinbase (within COINBASE_MATURITY of the tip).
599+
let input_with_immature_coinbase = setup_test_input(2_950).unwrap();
600+
assert!(policy
601+
.check_input_spendability(&[input_with_immature_coinbase], tip)
602+
.is_err());
603+
}
604+
605+
#[test]
606+
fn test_input_script_type() {
607+
let policy = MempoolPolicy::default();
608+
let input = setup_test_input(2_000).unwrap();
609+
assert!(policy.check_input_script_type(&[input]).is_ok());
610+
}
611+
612+
#[test]
613+
fn test_custom_policy_overrides_default() {
614+
// A custom policy that allows only v3 (TRUC) transactions.
615+
static V3_ONLY: &[Version] = &[Version(3)];
616+
let policy = MempoolPolicy {
617+
allowed_versions: V3_ONLY,
618+
..MempoolPolicy::default()
619+
};
620+
621+
let tip = default_tip();
622+
let input = setup_test_input(2_000).unwrap();
623+
let output = create_output(p2tr_script(), 9_000);
624+
let (selection, mut tx) = build_selection_with_tx(&[input], &[output]);
625+
626+
// v2 (the default tx version) is now rejected.
627+
tx.version = Version::TWO;
628+
assert!(matches!(
629+
policy.check_post_selection(&selection, &tx, tip),
630+
Err(MempoolPolicyError::UnsupportedVersion(_))
631+
));
632+
633+
// v3 passes.
634+
tx.version = Version(3);
635+
assert!(policy.check_post_selection(&selection, &tx, tip).is_ok());
636+
}
637+
}

src/selection.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,4 +727,35 @@ mod tests {
727727
"should return UnsupportedVersion error for version < 2"
728728
);
729729
}
730+
731+
#[test]
732+
fn test_create_psbt_with_policy_surfaces_violations() -> anyhow::Result<()> {
733+
let input = setup_test_input(2_000)?;
734+
let output = Output::with_script(ScriptBuf::new(), Amount::from_sat(9_000));
735+
let selection = Selection {
736+
inputs: vec![input],
737+
outputs: vec![output],
738+
};
739+
740+
// Custom policy accepting only v3 (TRUC). Default PsbtParams
741+
// produces v2, so the policy must reject this PSBT.
742+
static V3_ONLY: &[Version] = &[Version(3)];
743+
let policy = MempoolPolicy {
744+
allowed_versions: V3_ONLY,
745+
..MempoolPolicy::default()
746+
};
747+
let tip = ChainTip {
748+
height: absolute::Height::from_consensus(3_000)?,
749+
mtp: absolute::Time::from_consensus(500_001_000)?,
750+
};
751+
752+
let result = selection.create_psbt_with_policy(PsbtParams::default(), &policy, tip);
753+
assert!(matches!(
754+
result,
755+
Err(CreatePsbtError::Policy(
756+
MempoolPolicyError::UnsupportedVersion(_)
757+
))
758+
));
759+
Ok(())
760+
}
730761
}

0 commit comments

Comments
 (0)