Skip to content

Commit e287a1f

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 aae03e4 commit e287a1f

1 file changed

Lines changed: 180 additions & 10 deletions

File tree

fuzz/src/chanmon_consistency.rs

Lines changed: 180 additions & 10 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);
@@ -1118,23 +1237,17 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
11181237
node_a_chans[1].short_channel_id.unwrap(),
11191238
node_a_chans[2].short_channel_id.unwrap(),
11201239
];
1121-
let chan_ab_chan_ids: [ChannelId; 3] = [
1122-
node_a_chans[0].channel_id,
1123-
node_a_chans[1].channel_id,
1124-
node_a_chans[2].channel_id,
1125-
];
1240+
let chan_ab_chan_ids: [ChannelId; 3] =
1241+
[node_a_chans[0].channel_id, node_a_chans[1].channel_id, node_a_chans[2].channel_id];
11261242
// Get SCIDs for all B-C channels (from node C's perspective)
11271243
let node_c_chans: Vec<_> = nodes[2].list_usable_channels();
11281244
let chan_bc_scids: [u64; 3] = [
11291245
node_c_chans[0].short_channel_id.unwrap(),
11301246
node_c_chans[1].short_channel_id.unwrap(),
11311247
node_c_chans[2].short_channel_id.unwrap(),
11321248
];
1133-
let chan_bc_chan_ids: [ChannelId; 3] = [
1134-
node_c_chans[0].channel_id,
1135-
node_c_chans[1].channel_id,
1136-
node_c_chans[2].channel_id,
1137-
];
1249+
let chan_bc_chan_ids: [ChannelId; 3] =
1250+
[node_c_chans[0].channel_id, node_c_chans[1].channel_id, node_c_chans[2].channel_id];
11381251
// Keep old names for backward compatibility in existing code
11391252
let chan_a = chan_ab_scids[0];
11401253
let chan_a_id = chan_ab_chan_ids[0];
@@ -1729,6 +1842,53 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
17291842
}
17301843
};
17311844

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

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

0 commit comments

Comments
 (0)