Skip to content

Commit

Permalink
Add send_probe and introduce probing cookies
Browse files Browse the repository at this point in the history
When we send payment probes, we generate the [`PaymentHash`] based on a
probing cookie secret and a random [`PaymentId`]. This allows us to
discern probes from real payments, without keeping additional state.
  • Loading branch information
tnull committed Jul 6, 2022
1 parent 790abc5 commit 40339b9
Show file tree
Hide file tree
Showing 8 changed files with 425 additions and 32 deletions.
2 changes: 1 addition & 1 deletion fuzz/src/full_stack.rs
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
106 changes: 90 additions & 16 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
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],

/// 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,46 @@ 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. This can be checked by calling
/// [`payment_is_probe`].
///
/// [`payment_is_probe`]: Self::payment_is_probe
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 +3886,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 +6696,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 +6993,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 +7216,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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit 40339b9

Please sign in to comment.