Skip to content

Commit 21d52e8

Browse files
committed
test: add test for standardness and mempool policy checks
1 parent a732205 commit 21d52e8

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
@@ -186,7 +186,7 @@ impl MempoolPolicy {
186186
{
187187
return Err(MempoolPolicyError::SelectionTxMismatch);
188188
}
189-
189+
190190
if !selection
191191
.outputs
192192
.iter()
@@ -351,3 +351,197 @@ impl core::fmt::Display for MempoolPolicyError {
351351

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

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)