Skip to content

Commit c98521b

Browse files
committed
test: add test for standardness and mempool policy checks
1 parent f4ed2be commit c98521b

4 files changed

Lines changed: 458 additions & 1 deletion

File tree

src/lib.rs

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

2325
pub use canonical_unspents::*;

src/policy.rs

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ impl MempoolPolicy {
185185
{
186186
return Err(MempoolPolicyError::SelectionTxMismatch);
187187
}
188-
188+
189189
if !selection
190190
.outputs
191191
.iter()
@@ -350,3 +350,197 @@ impl core::fmt::Display for MempoolPolicyError {
350350

351351
#[cfg(feature = "std")]
352352
impl std::error::Error for MempoolPolicyError {}
353+
354+
#[cfg(test)]
355+
mod tests {
356+
use super::*;
357+
use crate::{test_utils::*, Output};
358+
use alloc::vec::Vec;
359+
use bitcoin::{transaction::Version, Amount, ScriptBuf, Transaction, TxOut};
360+
361+
fn default_policy() -> MempoolPolicy {
362+
MempoolPolicy {
363+
tip_height: absolute::Height::from_consensus(3_000).unwrap(),
364+
tip_mtp: absolute::Time::from_consensus(500_001_000).unwrap(),
365+
}
366+
}
367+
368+
#[test]
369+
fn test_tx_version() {
370+
let policy = default_policy();
371+
let input = setup_test_input(2_000).unwrap();
372+
let output = create_output(p2tr_script(), 9_000);
373+
let (selection, mut tx) = build_selection_with_tx(&[input], &[output]);
374+
375+
// Default version is 2, which is standard.
376+
assert!(policy.check_all(&selection, &tx).is_ok());
377+
378+
// Test version 1, which is also standard.
379+
tx.version = Version::ONE;
380+
assert!(policy.check_all(&selection, &tx).is_ok());
381+
382+
// Version 3 (TRUC) is standard under v30+.
383+
tx.version = Version(3);
384+
assert!(policy.check_all(&selection, &tx).is_ok());
385+
386+
// Test version 4, which is non-standard.
387+
tx.version = Version(4);
388+
assert!(matches!(
389+
policy.check_all(&selection, &tx),
390+
Err(MempoolPolicyError::UnsupportedVersion(_))
391+
));
392+
}
393+
394+
#[test]
395+
fn test_tx_locktime() {
396+
let policy = default_policy();
397+
let input = setup_test_input(2_000).unwrap();
398+
let output = create_output(p2tr_script(), 9_000);
399+
let (selection, mut tx) = build_selection_with_tx(&[input], &[output]);
400+
401+
// Locktime exactly equal to the tip height.
402+
tx.lock_time = absolute::LockTime::from_consensus(3_000);
403+
assert!(policy.check_all(&selection, &tx).is_ok());
404+
405+
// Locktime below the tip height.
406+
tx.lock_time = absolute::LockTime::from_consensus(2_500);
407+
assert!(policy.check_all(&selection, &tx).is_ok());
408+
409+
// Locktime above the tip height.
410+
tx.lock_time = absolute::LockTime::from_consensus(3_001);
411+
assert!(matches!(
412+
policy.check_all(&selection, &tx),
413+
Err(MempoolPolicyError::LockTimeNotMet(_))
414+
));
415+
416+
// Locktime one second below the tip MTP.
417+
tx.lock_time = absolute::LockTime::from_consensus(500_000_999);
418+
assert!(policy.check_all(&selection, &tx).is_ok());
419+
420+
// Locktime exactly equal to the tip MTP.
421+
tx.lock_time = absolute::LockTime::from_consensus(500_001_000);
422+
assert!(matches!(
423+
policy.check_all(&selection, &tx),
424+
Err(MempoolPolicyError::LockTimeNotMet(_))
425+
));
426+
427+
// Locktime above the tip MTP.
428+
tx.lock_time = absolute::LockTime::from_consensus(500_002_000);
429+
assert!(matches!(
430+
policy.check_all(&selection, &tx),
431+
Err(MempoolPolicyError::LockTimeNotMet(_))
432+
));
433+
}
434+
435+
#[test]
436+
fn test_max_tx_weight() {
437+
let policy = default_policy();
438+
439+
// A normal transaction with 1 input and 1 output.
440+
let input = setup_test_input(2_000).unwrap();
441+
let output = create_output(p2tr_script(), 9_000);
442+
let (selection, tx) = build_selection_with_tx(core::slice::from_ref(&input), &[output]);
443+
assert!(policy.check_all(&selection, &tx).is_ok());
444+
445+
// Heavy transaction with excess weight.
446+
let outputs_with_excess_weight: Vec<Output> = (0..2_350)
447+
.map(|_| create_output(p2tr_script(), 1_000))
448+
.collect();
449+
450+
let (_, heavy_tx) =
451+
build_selection_with_tx(&[input], outputs_with_excess_weight.as_slice());
452+
453+
assert!(heavy_tx.weight().to_wu() > MAX_STANDARD_TX_WEIGHT as u64);
454+
assert!(matches!(
455+
policy.check_max_tx_weight(heavy_tx.weight(), heavy_tx.version),
456+
Err(MempoolPolicyError::MaxWeightExceeded { .. })
457+
));
458+
}
459+
460+
#[test]
461+
fn test_tx_min_non_witness_size() {
462+
let policy = default_policy();
463+
let input = setup_test_input(2_000).unwrap();
464+
let output = create_output(p2tr_script(), 9_000);
465+
466+
// Transaction with 1 input and 1 output.
467+
let (selection, tx) = build_selection_with_tx(&[input], &[output]);
468+
assert!(policy.check_all(&selection, &tx).is_ok());
469+
470+
// Transaction with no inputs and 1 output.
471+
let tx_below_min_non_witness_size = Transaction {
472+
version: Version::TWO,
473+
lock_time: absolute::LockTime::ZERO,
474+
input: vec![],
475+
output: vec![TxOut {
476+
script_pubkey: ScriptBuf::new(),
477+
value: Amount::ZERO,
478+
}],
479+
};
480+
let empty_selection = Selection {
481+
inputs: vec![],
482+
outputs: vec![Output::with_script(ScriptBuf::new(), Amount::ZERO)],
483+
};
484+
assert!(
485+
tx_below_min_non_witness_size.base_size() < MIN_STANDARD_TX_NONWITNESS_SIZE as usize
486+
);
487+
assert!(matches!(
488+
policy.check_all(&empty_selection, &tx_below_min_non_witness_size),
489+
Err(MempoolPolicyError::TxTooSmall { .. })
490+
));
491+
}
492+
493+
#[test]
494+
fn test_min_fee_relay() {
495+
let policy = default_policy();
496+
497+
// Sufficient fee passes.
498+
let input = setup_test_input(2_000).unwrap();
499+
let output = create_output(p2tr_script(), 9_000);
500+
501+
let (selection, tx) = build_selection_with_tx(&[input], &[output]);
502+
assert!(policy.check_all(&selection, &tx).is_ok());
503+
504+
// Fee below the 1 sat/vB minimum is rejected.
505+
let input_with_insufficient_fee = setup_test_input(2_000).unwrap();
506+
let output_with_insufficient_fee = create_output(p2tr_script(), 9_999);
507+
508+
let (selection_with_insufficient_fee, tx_with_insufficient_fee) = build_selection_with_tx(
509+
&[input_with_insufficient_fee],
510+
&[output_with_insufficient_fee],
511+
);
512+
assert!(matches!(
513+
policy.check_all(&selection_with_insufficient_fee, &tx_with_insufficient_fee),
514+
Err(MempoolPolicyError::MinRelayFeeNotMet { .. })
515+
));
516+
}
517+
518+
#[test]
519+
fn test_max_witness_stack() {
520+
let policy = default_policy();
521+
let input = setup_test_input(2_000).unwrap();
522+
523+
assert!(policy.check_max_witness_stack(&[input]).is_ok());
524+
}
525+
526+
#[test]
527+
fn test_input_spendability() {
528+
let policy = default_policy();
529+
// A confirmed input
530+
let input = setup_test_input(2_000).unwrap();
531+
assert!(policy.check_input_spendability(&[input]).is_ok());
532+
533+
// An immature input
534+
let input_with_immature_coinbase = setup_test_input(2_950).unwrap();
535+
assert!(policy
536+
.check_input_spendability(&[input_with_immature_coinbase])
537+
.is_err());
538+
}
539+
540+
#[test]
541+
fn test_input_script_type() {
542+
let policy = default_policy();
543+
let input = setup_test_input(2_000).unwrap();
544+
assert!(policy.check_input_script_type(&[input]).is_ok());
545+
}
546+
}

src/selector.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,3 +687,128 @@ impl<'c> Selector<'c> {
687687
})
688688
}
689689
}
690+
691+
#[cfg(test)]
692+
mod tests {
693+
use super::*;
694+
use crate::test_utils::*;
695+
use bitcoin::Amount;
696+
697+
fn test_builder() -> SelectorParamsBuilder {
698+
SelectorParams::builder(
699+
FeeRate::from_sat_per_vb_unchecked(1),
700+
ChangeScript::from_script(p2tr_script(), Weight::from_wu(70)),
701+
)
702+
}
703+
704+
#[test]
705+
fn test_new_skips_validation() {
706+
// The unvalidated selection.
707+
let params = SelectorParams::new(
708+
FeeRate::from_sat_per_vb_unchecked(1),
709+
vec![create_output(p2tr_script(), 1)], // dust
710+
ChangeScript::from_script(p2tr_script(), Weight::from_wu(70)),
711+
);
712+
// Construction succeeds; explicit validation would fail.
713+
assert!(matches!(
714+
params.check_standardness(),
715+
Err(SelectorParamsError::DustOutput { .. })
716+
));
717+
}
718+
719+
#[test]
720+
fn test_dust_output() {
721+
let script = p2tr_script();
722+
let dust_limit = script.minimal_non_dust();
723+
let below_dust = dust_limit.to_sat() - 1;
724+
725+
// Output exactly at the minimum non-dust value.
726+
assert!(test_builder()
727+
.add_output(create_output(script.clone(), dust_limit.to_sat()))
728+
.build()
729+
.is_ok());
730+
731+
// OP_RETURN outputs are exempt from the dust check.
732+
assert!(test_builder()
733+
.add_output(create_output(op_return_script(b"test data"), 0))
734+
.build()
735+
.is_ok());
736+
737+
// Below the dust threshold reports the actual and required values.
738+
match test_builder()
739+
.add_output(create_output(script, below_dust))
740+
.build()
741+
{
742+
Err(SelectorParamsError::DustOutput { actual, required }) => {
743+
assert_eq!(actual, Amount::from_sat(below_dust));
744+
assert_eq!(required, dust_limit);
745+
}
746+
other => panic!("expected DustOutput error, got {:?}", other),
747+
}
748+
}
749+
750+
#[test]
751+
fn test_op_return_policy() {
752+
// A single zero-value OP_RETURN.
753+
assert!(test_builder()
754+
.add_output(create_output(op_return_script(b"first message"), 0))
755+
.build()
756+
.is_ok());
757+
758+
// OP_RETURN with non-zero value is rejected.
759+
assert!(matches!(
760+
test_builder()
761+
.add_output(create_output(op_return_script(b"data"), 1))
762+
.build(),
763+
Err(SelectorParamsError::OpReturnWithValue)
764+
));
765+
766+
// A single large OP_RETURN well under the cap passes.
767+
let large_but_ok = op_return_script(&vec![0xab; 50_000]);
768+
assert!(test_builder()
769+
.add_output(create_output(large_but_ok, 0))
770+
.build()
771+
.is_ok());
772+
773+
// Two OP_RETURNs that individually fit but together exceed the
774+
// aggregate cap are rejected.
775+
let half_one = op_return_script_large(&vec![0xab; 60_000]);
776+
let half_two = op_return_script_large(&vec![0xcd; 60_000]);
777+
match test_builder()
778+
.add_outputs(vec![create_output(half_one, 0), create_output(half_two, 0)])
779+
.build()
780+
{
781+
Err(SelectorParamsError::OpReturnTooLarge { actual, max }) => {
782+
assert!(actual > max);
783+
assert_eq!(max, MAX_OP_RETURN_BYTES);
784+
}
785+
other => panic!("expected OpReturnTooLarge, got {:?}", other),
786+
}
787+
788+
// A single OP_RETURN coexists with regular outputs.
789+
assert!(test_builder()
790+
.add_outputs(vec![
791+
create_output(p2tr_script(), 50_000),
792+
create_output(p2tr_script(), 30_000),
793+
create_output(op_return_script(b"memo"), 0),
794+
])
795+
.build()
796+
.is_ok());
797+
}
798+
#[test]
799+
fn test_output_script_type() {
800+
// Standard P2TR output passes.
801+
assert!(test_builder()
802+
.add_output(create_output(p2tr_script(), 10_000))
803+
.build()
804+
.is_ok());
805+
806+
// Non-standard script is rejected.
807+
assert!(matches!(
808+
test_builder()
809+
.add_output(create_output(non_standard_script(), 10_000))
810+
.build(),
811+
Err(SelectorParamsError::NonStandardScriptType)
812+
));
813+
}
814+
}

0 commit comments

Comments
 (0)