Skip to content

Commit e0ba40e

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 2a9ddaa commit e0ba40e

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);
@@ -1724,6 +1843,53 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
17241843
}
17251844
};
17261845

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

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

0 commit comments

Comments
 (0)