Skip to content

Commit 61caa03

Browse files
joostjagerclaude
andcommitted
fuzz: add MPP payment support to chanmon_consistency
Add multi-path payment (MPP) fuzzing commands that split payments across multiple channels: - send_mpp_payment: direct MPP from source to dest using multiple channels - send_mpp_hop_payment: MPP via intermediate node with multiple channels on either or both hops New fuzz commands: - 0x70: direct MPP 0->1 (uses all 3 A-B channels) - 0x71: MPP 0->1->2, multi channels on first hop (A-B) - 0x72: MPP 0->1->2, multi channels on both hops (A-B and B-C) - 0x73: MPP 0->1->2, multi channels on second hop (B-C) - 0x74: single-channel MPP 0->1 (all parts over one channel) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 24c492d commit 61caa03

File tree

1 file changed

+178
-0
lines changed

1 file changed

+178
-0
lines changed

fuzz/src/chanmon_consistency.rs

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,125 @@ fn send_hop_payment(
658658
}
659659
}
660660

661+
/// Send an MPP payment directly from source to dest using multiple channels.
662+
#[inline]
663+
fn send_mpp_payment(
664+
source: &ChanMan, dest: &ChanMan, dest_scids: &[u64], amt: u64, payment_secret: PaymentSecret,
665+
payment_hash: PaymentHash, payment_id: PaymentId,
666+
) -> bool {
667+
let num_paths = dest_scids.len();
668+
if num_paths == 0 {
669+
return false;
670+
}
671+
672+
let amt_per_path = amt / num_paths as u64;
673+
let mut paths = Vec::with_capacity(num_paths);
674+
675+
for (i, &dest_scid) in dest_scids.iter().enumerate() {
676+
let path_amt = if i == num_paths - 1 {
677+
amt - amt_per_path * (num_paths as u64 - 1)
678+
} else {
679+
amt_per_path
680+
};
681+
682+
paths.push(Path {
683+
hops: vec![RouteHop {
684+
pubkey: dest.get_our_node_id(),
685+
node_features: dest.node_features(),
686+
short_channel_id: dest_scid,
687+
channel_features: dest.channel_features(),
688+
fee_msat: path_amt,
689+
cltv_expiry_delta: 200,
690+
maybe_announced_channel: true,
691+
}],
692+
blinded_tail: None,
693+
});
694+
}
695+
696+
let route_params = RouteParameters::from_payment_params_and_value(
697+
PaymentParameters::from_node_id(dest.get_our_node_id(), TEST_FINAL_CLTV),
698+
amt,
699+
);
700+
let route = Route { paths, route_params: Some(route_params) };
701+
let onion = RecipientOnionFields::secret_only(payment_secret);
702+
let res = source.send_payment_with_route(route, payment_hash, onion, payment_id);
703+
match res {
704+
Err(_) => false,
705+
Ok(()) => check_payment_send_events(source, payment_id),
706+
}
707+
}
708+
709+
/// Send an MPP payment from source to dest via middle node.
710+
/// Supports multiple channels on either or both hops.
711+
#[inline]
712+
fn send_mpp_hop_payment(
713+
source: &ChanMan, middle: &ChanMan, middle_scids: &[u64], dest: &ChanMan, dest_scids: &[u64],
714+
amt: u64, payment_secret: PaymentSecret, payment_hash: PaymentHash, payment_id: PaymentId,
715+
) -> bool {
716+
// Create paths by pairing middle_scids with dest_scids
717+
let num_paths = middle_scids.len().max(dest_scids.len());
718+
if num_paths == 0 {
719+
return false;
720+
}
721+
722+
let first_hop_fee = 50_000;
723+
let amt_per_path = amt / num_paths as u64;
724+
let fee_per_path = first_hop_fee / num_paths as u64;
725+
let mut paths = Vec::with_capacity(num_paths);
726+
727+
for i in 0..num_paths {
728+
let middle_scid = middle_scids[i % middle_scids.len()];
729+
let dest_scid = dest_scids[i % dest_scids.len()];
730+
731+
let path_amt = if i == num_paths - 1 {
732+
amt - amt_per_path * (num_paths as u64 - 1)
733+
} else {
734+
amt_per_path
735+
};
736+
let path_fee = if i == num_paths - 1 {
737+
first_hop_fee - fee_per_path * (num_paths as u64 - 1)
738+
} else {
739+
fee_per_path
740+
};
741+
742+
paths.push(Path {
743+
hops: vec![
744+
RouteHop {
745+
pubkey: middle.get_our_node_id(),
746+
node_features: middle.node_features(),
747+
short_channel_id: middle_scid,
748+
channel_features: middle.channel_features(),
749+
fee_msat: path_fee,
750+
cltv_expiry_delta: 100,
751+
maybe_announced_channel: true,
752+
},
753+
RouteHop {
754+
pubkey: dest.get_our_node_id(),
755+
node_features: dest.node_features(),
756+
short_channel_id: dest_scid,
757+
channel_features: dest.channel_features(),
758+
fee_msat: path_amt,
759+
cltv_expiry_delta: 200,
760+
maybe_announced_channel: true,
761+
},
762+
],
763+
blinded_tail: None,
764+
});
765+
}
766+
767+
let route_params = RouteParameters::from_payment_params_and_value(
768+
PaymentParameters::from_node_id(dest.get_our_node_id(), TEST_FINAL_CLTV),
769+
amt,
770+
);
771+
let route = Route { paths, route_params: Some(route_params) };
772+
let onion = RecipientOnionFields::secret_only(payment_secret);
773+
let res = source.send_payment_with_route(route, payment_hash, onion, payment_id);
774+
match res {
775+
Err(_) => false,
776+
Ok(()) => check_payment_send_events(source, payment_id),
777+
}
778+
}
779+
661780
#[inline]
662781
pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
663782
let out = SearchingOutput::new(underlying_out);
@@ -1726,6 +1845,53 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
17261845
}
17271846
};
17281847

1848+
// Direct MPP payment (no hop)
1849+
let send_mpp_direct = |source_idx: usize,
1850+
dest_idx: usize,
1851+
dest_scids: &[u64],
1852+
amt: u64,
1853+
payment_ctr: &mut u64| {
1854+
let source = &nodes[source_idx];
1855+
let dest = &nodes[dest_idx];
1856+
let (secret, hash) = get_payment_secret_hash(dest, payment_ctr);
1857+
let mut id = PaymentId([0; 32]);
1858+
id.0[0..8].copy_from_slice(&payment_ctr.to_ne_bytes());
1859+
let succeeded = send_mpp_payment(source, dest, dest_scids, amt, secret, hash, id);
1860+
if succeeded {
1861+
pending_payments.borrow_mut()[source_idx].push(id);
1862+
}
1863+
};
1864+
1865+
// MPP payment via hop - splits payment across multiple channels on either or both hops
1866+
let send_mpp_hop = |source_idx: usize,
1867+
middle_idx: usize,
1868+
middle_scids: &[u64],
1869+
dest_idx: usize,
1870+
dest_scids: &[u64],
1871+
amt: u64,
1872+
payment_ctr: &mut u64| {
1873+
let source = &nodes[source_idx];
1874+
let middle = &nodes[middle_idx];
1875+
let dest = &nodes[dest_idx];
1876+
let (secret, hash) = get_payment_secret_hash(dest, payment_ctr);
1877+
let mut id = PaymentId([0; 32]);
1878+
id.0[0..8].copy_from_slice(&payment_ctr.to_ne_bytes());
1879+
let succeeded = send_mpp_hop_payment(
1880+
source,
1881+
middle,
1882+
middle_scids,
1883+
dest,
1884+
dest_scids,
1885+
amt,
1886+
secret,
1887+
hash,
1888+
id,
1889+
);
1890+
if succeeded {
1891+
pending_payments.borrow_mut()[source_idx].push(id);
1892+
}
1893+
};
1894+
17291895
let v = get_slice!(1)[0];
17301896
out.locked_write(format!("READ A BYTE! HANDLING INPUT {:x}...........\n", v).as_bytes());
17311897
match v {
@@ -1910,6 +2076,18 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
19102076
0x6c => send_hop_noret(0, 1, chan_a, 2, chan_b, 1, &mut p_ctr),
19112077
0x6d => send_hop_noret(2, 1, chan_b, 0, chan_a, 1, &mut p_ctr),
19122078

2079+
// MPP payments
2080+
// 0x70: direct MPP from 0 to 1 (multi A-B channels)
2081+
0x70 => send_mpp_direct(0, 1, &chan_ab_scids, 1_000_000, &mut p_ctr),
2082+
// 0x71: MPP 0->1->2, multi channels on first hop (A-B)
2083+
0x71 => send_mpp_hop(0, 1, &chan_ab_scids, 2, &[chan_b], 1_000_000, &mut p_ctr),
2084+
// 0x72: MPP 0->1->2, multi channels on both hops (A-B and B-C)
2085+
0x72 => send_mpp_hop(0, 1, &chan_ab_scids, 2, &chan_bc_scids, 1_000_000, &mut p_ctr),
2086+
// 0x73: MPP 0->1->2, multi channels on second hop (B-C)
2087+
0x73 => send_mpp_hop(0, 1, &[chan_a], 2, &chan_bc_scids, 1_000_000, &mut p_ctr),
2088+
// 0x74: direct MPP from 0 to 1, multi parts over single channel
2089+
0x74 => send_mpp_direct(0, 1, &[chan_a, chan_a, chan_a], 1_000_000, &mut p_ctr),
2090+
19132091
0x80 => {
19142092
let mut max_feerate = last_htlc_clear_fee_a;
19152093
if !anchors {

0 commit comments

Comments
 (0)