Allow a single P2A output to be below dust in `check_spends!`
What changed, and why it matters
This commit changes an internal test helper, not production code. It relaxes a test-only dust-check so that transactions containing one special below-dust output (a shared anchor used in zero-fee commitment transactions) no longer fail the assertion. There is no direct security vulnerability in the change itself; it is a test-infrastructure update to match new expected on-chain behavior.
No immediate action required. Treat as a normal test-maintenance commit. Reviewers should confirm that the relaxed dust rule correctly mirrors the intended on-chain policy for P2A anchors in zero-fee commitment transactions and that the new zero-fee and empty-witness assertions are sufficient to prevent misuse in tests.
Security signals we found
Change is confined to test utility code (`functional_test_utils.rs`)
Relaxes a dust-limit assertion for a single P2A output
Adds zero-fee requirement when a below-dust P2A output exists
Adds witness-empty check for P2A inputs
No production consensus, cryptography, or networking code changed
Evidence from the diff
The diff modifies do_check_spends and the check_spends! macro in lightning/src/ln/functional_test_utils.rs. Previously these helpers required every transaction output to be at or above minimal_non_dust(). The patch allows exactly one output to be below dust if its script pubkey matches shared_anchor_script_pubkey() (the P2A / pay-to-anchor output used in zero-fee commitment transactions). It also adds a rule that when such a below-dust P2A output exists, the transaction must be zero-fee (inputs equal outputs), and skips the 1 sat/vbyte minimum-fee check when a P2A output is present. Additionally, it asserts that spending a P2A input has an empty witness. The change only affects test assertions; no consensus or wallet logic is altered.
Changed components
lightning/src/ln/functional_test_utils.rsTest helper `do_check_spends`Test macro `check_spends!`Inspect captured patch +34 / −9
diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs
index 4fc7635..9a4038c 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -1941,23 +1941,41 @@ pub fn update_nodes_with_chan_announce<'a, 'b, 'c, 'd>(
pub fn do_check_spends<F: Fn(&bitcoin::transaction::OutPoint) -> Option<TxOut>>(
tx: &Transaction, get_output: F,
) {
+ let mut p2a_output_below_dust = false;
+ let mut has_p2a_output = false;
for outp in tx.output.iter() {
- assert!(
- outp.value >= outp.script_pubkey.minimal_non_dust(),
- "Spending tx output didn't meet dust limit"
- );
+ let is_p2a = outp.script_pubkey == crate::ln::chan_utils::shared_anchor_script_pubkey();
+ has_p2a_output |= is_p2a;
+ if outp.value < outp.script_pubkey.minimal_non_dust() {
+ if p2a_output_below_dust || !is_p2a {
+ panic!("Spending tx output didn't meet dust limit");
+ }
+ p2a_output_below_dust = true;
+ };
}
let mut total_value_in = 0;
for input in tx.input.iter() {
- total_value_in += get_output(&input.previous_output).unwrap().value.to_sat();
+ let output = get_output(&input.previous_output).unwrap();
+ if output.script_pubkey == crate::ln::chan_utils::shared_anchor_script_pubkey() {
+ assert!(input.witness.is_empty());
+ }
+ total_value_in += output.value.to_sat();
}
let mut total_value_out = 0;
for output in tx.output.iter() {
total_value_out += output.value.to_sat();
}
- let min_fee = (tx.weight().to_wu() as u64 + 3) / 4; // One sat per vbyte (ie per weight/4, rounded up)
- // Input amount - output amount = fee, so check that out + min_fee is smaller than input
- assert!(total_value_out + min_fee <= total_value_in);
+ if p2a_output_below_dust {
+ assert_eq!(
+ total_value_in, total_value_out,
+ "Spending tx has one output below dust, while not zero fee"
+ );
+ // 0FC commitment transactions will have their fee bumped by a child, so don't require that
+ // they meet the 1sat/vB minimum.
+ } else if !has_p2a_output {
+ let min_fee = (tx.weight().to_wu() as u64 + 3) / 4; // One sat per vbyte (ie per weight/4, rounded up)
+ assert!(total_value_out + min_fee <= total_value_in);
+ }
tx.verify(get_output).unwrap();
}
@@ -1966,8 +1984,15 @@ macro_rules! check_spends {
($tx: expr, $($spends_txn: expr),*) => {
{
$(
+ let mut single_output_below_dust = false;
for outp in $spends_txn.output.iter() {
- assert!(outp.value >= outp.script_pubkey.minimal_non_dust(), "Input tx output didn't meet dust limit");
+ if outp.value < outp.script_pubkey.minimal_non_dust() {
+ if single_output_below_dust || outp.script_pubkey != $crate::ln::chan_utils::shared_anchor_script_pubkey() {
+ panic!("Input tx output didn't meet dust limit");
+ } else {
+ single_output_below_dust = true;
+ }
+ }
}
)*
let get_output = |out_point: &bitcoin::transaction::OutPoint| {
Why this scored 16/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.