Skip to content

Commit 3038033

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) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d6c5bff commit 3038033

1 file changed

Lines changed: 176 additions & 0 deletions

File tree

fuzz/src/chanmon_consistency.rs

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,125 @@ fn send_hop_payment(
656656
}
657657
}
658658

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

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

2073+
// MPP payments
2074+
// 0x70: direct MPP from 0 to 1 (multi A-B channels)
2075+
0x70 => send_mpp_direct(0, 1, &chan_ab_scids, 1_000_000, &mut p_ctr),
2076+
// 0x71: MPP 0->1->2, multi channels on first hop (A-B)
2077+
0x71 => send_mpp_hop(0, 1, &chan_ab_scids, 2, &[chan_b], 1_000_000, &mut p_ctr),
2078+
// 0x72: MPP 0->1->2, multi channels on both hops (A-B and B-C)
2079+
0x72 => send_mpp_hop(0, 1, &chan_ab_scids, 2, &chan_bc_scids, 1_000_000, &mut p_ctr),
2080+
// 0x73: MPP 0->1->2, multi channels on second hop (B-C)
2081+
0x73 => send_mpp_hop(0, 1, &[chan_a], 2, &chan_bc_scids, 1_000_000, &mut p_ctr),
2082+
19072083
0x80 => {
19082084
let mut max_feerate = last_htlc_clear_fee_a;
19092085
if !anchors {

0 commit comments

Comments
 (0)