Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add simple probing API #1567

Merged
merged 2 commits into from Jul 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions fuzz/src/chanmon_consistency.rs
Expand Up @@ -850,6 +850,12 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out) {
events::Event::PaymentClaimed { .. } => {},
events::Event::PaymentPathSuccessful { .. } => {},
events::Event::PaymentPathFailed { .. } => {},
events::Event::ProbeSuccessful { .. } | events::Event::ProbeFailed { .. } => {
// Even though we don't explicitly send probes, because probes are
// detected based on hashing the payment hash+preimage, its rather
// trivial for the fuzzer to build payments that accidentally end up
// looking like probes.
},
events::Event::PaymentForwarded { .. } if $node == 1 => {},
events::Event::PendingHTLCsForwardable { .. } => {
nodes[$node].process_pending_htlc_forwards();
Expand Down
2 changes: 1 addition & 1 deletion fuzz/src/full_stack.rs
Expand Up @@ -393,7 +393,7 @@ pub fn do_test(data: &[u8], logger: &Arc<dyn Logger>) {
// Adding new calls to `KeysInterface::get_secure_random_bytes` during startup can change all the
// keys subsequently generated in this test. Rather than regenerating all the messages manually,
// it's easier to just increment the counter here so the keys don't change.
keys_manager.counter.fetch_sub(1, Ordering::AcqRel);
keys_manager.counter.fetch_sub(2, Ordering::AcqRel);
let our_id = PublicKey::from_secret_key(&Secp256k1::signing_only(), &keys_manager.get_node_secret(Recipient::Node).unwrap());
let network_graph = Arc::new(NetworkGraph::new(genesis_block(network).block_hash(), Arc::clone(&logger)));
let gossip_sync = Arc::new(P2PGossipSync::new(Arc::clone(&network_graph), None, Arc::clone(&logger)));
Expand Down
92 changes: 80 additions & 12 deletions lightning-invoice/src/payment.rs
Expand Up @@ -94,6 +94,8 @@
//! # ) -> u64 { 0 }
//! # fn payment_path_failed(&mut self, _path: &[&RouteHop], _short_channel_id: u64) {}
//! # fn payment_path_successful(&mut self, _path: &[&RouteHop]) {}
//! # fn probe_failed(&mut self, _path: &[&RouteHop], _short_channel_id: u64) {}
//! # fn probe_successful(&mut self, _path: &[&RouteHop]) {}
//! # }
//! #
//! # struct FakeLogger {}
Expand Down Expand Up @@ -584,6 +586,18 @@ where
.map_or(1, |attempts| attempts.count + 1);
log_trace!(self.logger, "Payment {} succeeded (attempts: {})", log_bytes!(payment_hash.0), attempts);
},
Event::ProbeSuccessful { payment_hash, path, .. } => {
log_trace!(self.logger, "Probe payment {} of {}msat was successful", log_bytes!(payment_hash.0), path.last().unwrap().fee_msat);
let path = path.iter().collect::<Vec<_>>();
self.scorer.lock().probe_successful(&path);
},
Event::ProbeFailed { payment_hash, path, short_channel_id, .. } => {
if let Some(short_channel_id) = short_channel_id {
log_trace!(self.logger, "Probe payment {} of {}msat failed at channel {}", log_bytes!(payment_hash.0), path.last().unwrap().fee_msat, *short_channel_id);
let path = path.iter().collect::<Vec<_>>();
self.scorer.lock().probe_failed(&path, *short_channel_id);
}
},
_ => {},
}

Expand Down Expand Up @@ -1296,7 +1310,7 @@ mod tests {
.expect_send(Amount::ForInvoice(final_value_msat))
.expect_send(Amount::OnRetry(final_value_msat / 2));
let router = TestRouter {};
let scorer = RefCell::new(TestScorer::new().expect(PaymentPath::Failure {
let scorer = RefCell::new(TestScorer::new().expect(TestResult::PaymentFailure {
path: path.clone(), short_channel_id: path[0].short_channel_id,
}));
let logger = TestLogger::new();
Expand Down Expand Up @@ -1332,8 +1346,8 @@ mod tests {
let payer = TestPayer::new().expect_send(Amount::ForInvoice(final_value_msat));
let router = TestRouter {};
let scorer = RefCell::new(TestScorer::new()
.expect(PaymentPath::Success { path: route.paths[0].clone() })
.expect(PaymentPath::Success { path: route.paths[1].clone() })
.expect(TestResult::PaymentSuccess { path: route.paths[0].clone() })
.expect(TestResult::PaymentSuccess { path: route.paths[1].clone() })
);
let logger = TestLogger::new();
let invoice_payer =
Expand Down Expand Up @@ -1416,13 +1430,15 @@ mod tests {
}

struct TestScorer {
expectations: Option<VecDeque<PaymentPath>>,
expectations: Option<VecDeque<TestResult>>,
}

#[derive(Debug)]
enum PaymentPath {
Failure { path: Vec<RouteHop>, short_channel_id: u64 },
Success { path: Vec<RouteHop> },
enum TestResult {
PaymentFailure { path: Vec<RouteHop>, short_channel_id: u64 },
PaymentSuccess { path: Vec<RouteHop> },
ProbeFailure { path: Vec<RouteHop>, short_channel_id: u64 },
ProbeSuccess { path: Vec<RouteHop> },
}

impl TestScorer {
Expand All @@ -1432,7 +1448,7 @@ mod tests {
}
}

fn expect(mut self, expectation: PaymentPath) -> Self {
fn expect(mut self, expectation: TestResult) -> Self {
self.expectations.get_or_insert_with(|| VecDeque::new()).push_back(expectation);
self
}
Expand All @@ -1451,13 +1467,19 @@ mod tests {
fn payment_path_failed(&mut self, actual_path: &[&RouteHop], actual_short_channel_id: u64) {
if let Some(expectations) = &mut self.expectations {
match expectations.pop_front() {
Some(PaymentPath::Failure { path, short_channel_id }) => {
Some(TestResult::PaymentFailure { path, short_channel_id }) => {
assert_eq!(actual_path, &path.iter().collect::<Vec<_>>()[..]);
assert_eq!(actual_short_channel_id, short_channel_id);
},
Some(PaymentPath::Success { path }) => {
Some(TestResult::PaymentSuccess { path }) => {
panic!("Unexpected successful payment path: {:?}", path)
},
Some(TestResult::ProbeFailure { path, .. }) => {
panic!("Unexpected failed payment probe: {:?}", path)
},
Some(TestResult::ProbeSuccess { path }) => {
panic!("Unexpected successful payment probe: {:?}", path)
},
None => panic!("Unexpected payment_path_failed call: {:?}", actual_path),
}
}
Expand All @@ -1466,10 +1488,56 @@ mod tests {
fn payment_path_successful(&mut self, actual_path: &[&RouteHop]) {
if let Some(expectations) = &mut self.expectations {
match expectations.pop_front() {
Some(PaymentPath::Failure { path, .. }) => {
Some(TestResult::PaymentFailure { path, .. }) => {
panic!("Unexpected payment path failure: {:?}", path)
},
Some(PaymentPath::Success { path }) => {
Some(TestResult::PaymentSuccess { path }) => {
assert_eq!(actual_path, &path.iter().collect::<Vec<_>>()[..]);
},
Some(TestResult::ProbeFailure { path, .. }) => {
panic!("Unexpected failed payment probe: {:?}", path)
},
Some(TestResult::ProbeSuccess { path }) => {
panic!("Unexpected successful payment probe: {:?}", path)
},
None => panic!("Unexpected payment_path_successful call: {:?}", actual_path),
}
}
}

fn probe_failed(&mut self, actual_path: &[&RouteHop], actual_short_channel_id: u64) {
if let Some(expectations) = &mut self.expectations {
match expectations.pop_front() {
Some(TestResult::PaymentFailure { path, .. }) => {
panic!("Unexpected failed payment path: {:?}", path)
},
Some(TestResult::PaymentSuccess { path }) => {
panic!("Unexpected successful payment path: {:?}", path)
},
Some(TestResult::ProbeFailure { path, short_channel_id }) => {
assert_eq!(actual_path, &path.iter().collect::<Vec<_>>()[..]);
assert_eq!(actual_short_channel_id, short_channel_id);
},
Some(TestResult::ProbeSuccess { path }) => {
panic!("Unexpected successful payment probe: {:?}", path)
},
None => panic!("Unexpected payment_path_failed call: {:?}", actual_path),
}
}
}
fn probe_successful(&mut self, actual_path: &[&RouteHop]) {
if let Some(expectations) = &mut self.expectations {
match expectations.pop_front() {
Some(TestResult::PaymentFailure { path, .. }) => {
panic!("Unexpected payment path failure: {:?}", path)
},
Some(TestResult::PaymentSuccess { path }) => {
panic!("Unexpected successful payment path: {:?}", path)
},
Some(TestResult::ProbeFailure { path, .. }) => {
panic!("Unexpected failed payment probe: {:?}", path)
},
Some(TestResult::ProbeSuccess { path }) => {
assert_eq!(actual_path, &path.iter().collect::<Vec<_>>()[..]);
},
None => panic!("Unexpected payment_path_successful call: {:?}", actual_path),
Expand Down
103 changes: 87 additions & 16 deletions lightning/src/ln/channelmanager.rs
Expand Up @@ -740,6 +740,11 @@ pub struct ChannelManager<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref,
/// [fake scids]: crate::util::scid_utils::fake_scid
fake_scid_rand_bytes: [u8; 32],

/// When we send payment probes, we generate the [`PaymentHash`] based on this cookie secret
/// and a random [`PaymentId`]. This allows us to discern probes from real payments, without
/// keeping additional state.
probing_cookie_secret: [u8; 32],
tnull marked this conversation as resolved.
Show resolved Hide resolved

/// Used to track the last value sent in a node_announcement "timestamp" field. We ensure this
/// value increases strictly since we don't assume access to a time source.
last_node_announcement_serial: AtomicUsize,
Expand Down Expand Up @@ -1589,6 +1594,8 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
inbound_payment_key: expanded_inbound_key,
fake_scid_rand_bytes: keys_manager.get_secure_random_bytes(),

probing_cookie_secret: keys_manager.get_secure_random_bytes(),

last_node_announcement_serial: AtomicUsize::new(0),
highest_seen_timestamp: AtomicUsize::new(0),

Expand Down Expand Up @@ -2731,6 +2738,43 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
}
}

/// Send a payment that is probing the given route for liquidity. We calculate the
/// [`PaymentHash`] of probes based on a static secret and a random [`PaymentId`], which allows
/// us to easily discern them from real payments.
pub fn send_probe(&self, hops: Vec<RouteHop>) -> Result<(PaymentHash, PaymentId), PaymentSendFailure> {
let payment_id = PaymentId(self.keys_manager.get_secure_random_bytes());

let payment_hash = self.probing_cookie_from_id(&payment_id);

if hops.len() < 2 {
return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError {
err: "No need probing a path with less than two hops".to_string()
}))
}

let route = Route { paths: vec![hops], payment_params: None };

match self.send_payment_internal(&route, payment_hash, &None, None, Some(payment_id), None) {
Ok(payment_id) => Ok((payment_hash, payment_id)),
Err(e) => Err(e)
}
}

/// Returns whether a payment with the given [`PaymentHash`] and [`PaymentId`] is, in fact, a
/// payment probe.
pub(crate) fn payment_is_probe(&self, payment_hash: &PaymentHash, payment_id: &PaymentId) -> bool {
let target_payment_hash = self.probing_cookie_from_id(payment_id);
target_payment_hash == *payment_hash
}

/// Returns the 'probing cookie' for the given [`PaymentId`].
fn probing_cookie_from_id(&self, payment_id: &PaymentId) -> PaymentHash {
let mut preimage = [0u8; 64];
preimage[..32].copy_from_slice(&self.probing_cookie_secret);
preimage[32..].copy_from_slice(&payment_id.0);
PaymentHash(Sha256::hash(&preimage).into_inner())
}

/// Handles the generation of a funding transaction, optionally (for tests) with a function
/// which checks the correctness of the funding transaction given the associated channel.
fn funding_transaction_generated_intern<FundingOutput: Fn(&Channel<Signer>, &Transaction) -> Result<OutPoint, APIError>>(
Expand Down Expand Up @@ -3839,22 +3883,40 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
let (network_update, short_channel_id, payment_retryable, onion_error_code, onion_error_data) = onion_utils::process_onion_failure(&self.secp_ctx, &self.logger, &source, err.data.clone());
#[cfg(not(test))]
let (network_update, short_channel_id, payment_retryable, _, _) = onion_utils::process_onion_failure(&self.secp_ctx, &self.logger, &source, err.data.clone());
// TODO: If we decided to blame ourselves (or one of our channels) in
// process_onion_failure we should close that channel as it implies our
// next-hop is needlessly blaming us!
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
rejected_by_dest: !payment_retryable,
network_update,
all_paths_failed,
path: path.clone(),
short_channel_id,
retry,
#[cfg(test)]
error_code: onion_error_code,
#[cfg(test)]
error_data: onion_error_data

if self.payment_is_probe(payment_hash, &payment_id) {
if !payment_retryable {
events::Event::ProbeSuccessful {
payment_id,
payment_hash: payment_hash.clone(),
path: path.clone(),
}
} else {
events::Event::ProbeFailed {
payment_id: payment_id,
payment_hash: payment_hash.clone(),
path: path.clone(),
short_channel_id,
}
}
} else {
// TODO: If we decided to blame ourselves (or one of our channels) in
// process_onion_failure we should close that channel as it implies our
// next-hop is needlessly blaming us!
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
rejected_by_dest: !payment_retryable,
network_update,
all_paths_failed,
path: path.clone(),
short_channel_id,
retry,
#[cfg(test)]
error_code: onion_error_code,
#[cfg(test)]
error_data: onion_error_data
}
}
},
&HTLCFailReason::Reason {
Expand Down Expand Up @@ -6631,6 +6693,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> Writeable f
(5, self.our_network_pubkey, required),
(7, self.fake_scid_rand_bytes, required),
(9, htlc_purposes, vec_type),
(11, self.probing_cookie_secret, required),
});

Ok(())
Expand Down Expand Up @@ -6927,18 +6990,24 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
let mut pending_outbound_payments = None;
let mut received_network_pubkey: Option<PublicKey> = None;
let mut fake_scid_rand_bytes: Option<[u8; 32]> = None;
let mut probing_cookie_secret: Option<[u8; 32]> = None;
let mut claimable_htlc_purposes = None;
read_tlv_fields!(reader, {
(1, pending_outbound_payments_no_retry, option),
(3, pending_outbound_payments, option),
(5, received_network_pubkey, option),
(7, fake_scid_rand_bytes, option),
(9, claimable_htlc_purposes, vec_type),
(11, probing_cookie_secret, option),
});
if fake_scid_rand_bytes.is_none() {
fake_scid_rand_bytes = Some(args.keys_manager.get_secure_random_bytes());
}

if probing_cookie_secret.is_none() {
probing_cookie_secret = Some(args.keys_manager.get_secure_random_bytes());
}

if pending_outbound_payments.is_none() && pending_outbound_payments_no_retry.is_none() {
pending_outbound_payments = Some(pending_outbound_payments_compat);
} else if pending_outbound_payments.is_none() {
Expand Down Expand Up @@ -7144,6 +7213,8 @@ impl<'a, Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
outbound_scid_aliases: Mutex::new(outbound_scid_aliases),
fake_scid_rand_bytes: fake_scid_rand_bytes.unwrap(),

probing_cookie_secret: probing_cookie_secret.unwrap(),

our_network_key,
our_network_pubkey,
secp_ctx,
Expand Down
6 changes: 3 additions & 3 deletions lightning/src/ln/mod.rs
Expand Up @@ -80,15 +80,15 @@ pub use self::peer_channel_encryptor::LN_MAX_MSG_LEN;
/// payment_hash type, use to cross-lock hop
/// (C-not exported) as we just use [u8; 32] directly
#[derive(Hash, Copy, Clone, PartialEq, Eq, Debug)]
pub struct PaymentHash(pub [u8;32]);
pub struct PaymentHash(pub [u8; 32]);
/// payment_preimage type, use to route payment between hop
/// (C-not exported) as we just use [u8; 32] directly
#[derive(Hash, Copy, Clone, PartialEq, Eq, Debug)]
pub struct PaymentPreimage(pub [u8;32]);
pub struct PaymentPreimage(pub [u8; 32]);
/// payment_secret type, use to authenticate sender to the receiver and tie MPP HTLCs together
/// (C-not exported) as we just use [u8; 32] directly
#[derive(Hash, Copy, Clone, PartialEq, Eq, Debug)]
pub struct PaymentSecret(pub [u8;32]);
pub struct PaymentSecret(pub [u8; 32]);

use prelude::*;
use bitcoin::bech32;
Expand Down