Skip to content

Add utility for retrieving an LDK payment preimage #1216

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

Merged
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 65 additions & 20 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,14 @@ use core::ops::Deref;
use std::time::Instant;

mod inbound_payment {
use alloc::string::ToString;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed?

Copy link
Contributor Author

@valentinewallace valentinewallace Dec 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cd lightning && cargo test --verbose --color always --no-default-features --features no-std (part of CI) fails without that line

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. Seems other uses are covered by use prelude::*;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get a compile warn here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, no, I meant I don't get a compile warn that this use is unnecessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, just haven't seen that use elsewhere in the repo when needing to_string

use bitcoin::hashes::{Hash, HashEngine};
use bitcoin::hashes::cmp::fixed_time_eq;
use bitcoin::hashes::hmac::{Hmac, HmacEngine};
use bitcoin::hashes::sha256::Hash as Sha256;
use chain::keysinterface::{KeyMaterial, KeysInterface, Sign};
use ln::{PaymentHash, PaymentPreimage, PaymentSecret};
use ln::channelmanager::APIError;
use ln::msgs;
use ln::msgs::MAX_VALUE_MSAT;
use util::chacha20::ChaCha20;
Expand Down Expand Up @@ -113,7 +115,7 @@ mod inbound_payment {

impl ExpandedKey {
pub(super) fn new(key_material: &KeyMaterial) -> ExpandedKey {
hkdf_extract_expand(&vec![0], &key_material)
hkdf_extract_expand(b"LDK Inbound Payment Key Expansion", &key_material)
}
}

Expand Down Expand Up @@ -237,15 +239,7 @@ mod inbound_payment {
pub(super) fn verify<L: Deref>(payment_hash: PaymentHash, payment_data: msgs::FinalOnionHopData, highest_seen_timestamp: u64, keys: &ExpandedKey, logger: &L) -> Result<Option<PaymentPreimage>, ()>
where L::Target: Logger
{
let mut iv_bytes = [0; IV_LEN];
let (iv_slice, encrypted_metadata_bytes) = payment_data.payment_secret.0.split_at(IV_LEN);
iv_bytes.copy_from_slice(iv_slice);

let chacha_block = ChaCha20::get_single_block(&keys.metadata_key, &iv_bytes);
let mut metadata_bytes: [u8; METADATA_LEN] = [0; METADATA_LEN];
for i in 0..METADATA_LEN {
metadata_bytes[i] = chacha_block[i] ^ encrypted_metadata_bytes[i];
}
let (iv_bytes, metadata_bytes) = decrypt_metadata(payment_data.payment_secret, keys);

let payment_type_res = Method::from_bits((metadata_bytes[0] & 0b1110_0000) >> METHOD_TYPE_OFFSET);
let mut amt_msat_bytes = [0; AMT_MSAT_LEN];
Expand All @@ -269,15 +263,13 @@ mod inbound_payment {
}
},
Ok(Method::LdkPaymentHash) => {
let mut hmac = HmacEngine::<Sha256>::new(&keys.ldk_pmt_hash_key);
hmac.input(&iv_bytes);
hmac.input(&metadata_bytes);
let decoded_payment_preimage = Hmac::from_engine(hmac).into_inner();
if !fixed_time_eq(&payment_hash.0, &Sha256::hash(&decoded_payment_preimage).into_inner()) {
log_trace!(logger, "Failing HTLC with payment_hash {}: payment preimage {} did not match", log_bytes!(payment_hash.0), log_bytes!(decoded_payment_preimage));
return Err(())
match derive_ldk_payment_preimage(payment_hash, &iv_bytes, &metadata_bytes, keys) {
Ok(preimage) => payment_preimage = Some(preimage),
Err(bad_preimage_bytes) => {
log_trace!(logger, "Failing HTLC with payment_hash {} due to mismatching preimage {}", log_bytes!(payment_hash.0), log_bytes!(bad_preimage_bytes));
return Err(())
}
}
payment_preimage = Some(PaymentPreimage(decoded_payment_preimage));
},
Err(unknown_bits) => {
log_trace!(logger, "Failing HTLC with payment hash {} due to unknown payment type {}", log_bytes!(payment_hash.0), unknown_bits);
Expand All @@ -298,6 +290,50 @@ mod inbound_payment {
Ok(payment_preimage)
}

pub(super) fn get_payment_preimage(payment_hash: PaymentHash, payment_secret: PaymentSecret, keys: &ExpandedKey) -> Result<PaymentPreimage, APIError> {
let (iv_bytes, metadata_bytes) = decrypt_metadata(payment_secret, keys);

match Method::from_bits((metadata_bytes[0] & 0b1110_0000) >> METHOD_TYPE_OFFSET) {
Ok(Method::LdkPaymentHash) => {
derive_ldk_payment_preimage(payment_hash, &iv_bytes, &metadata_bytes, keys)
.map_err(|bad_preimage_bytes| APIError::APIMisuseError {
err: format!("Payment hash {} did not match decoded preimage {}", log_bytes!(payment_hash.0), log_bytes!(bad_preimage_bytes))
})
},
Ok(Method::UserPaymentHash) => Err(APIError::APIMisuseError {
err: "Expected payment type to be LdkPaymentHash, instead got UserPaymentHash".to_string()
}),
Err(other) => Err(APIError::APIMisuseError { err: format!("Unknown payment type: {}", other) }),
}
}

fn decrypt_metadata(payment_secret: PaymentSecret, keys: &ExpandedKey) -> ([u8; IV_LEN], [u8; METADATA_LEN]) {
let mut iv_bytes = [0; IV_LEN];
let (iv_slice, encrypted_metadata_bytes) = payment_secret.0.split_at(IV_LEN);
iv_bytes.copy_from_slice(iv_slice);

let chacha_block = ChaCha20::get_single_block(&keys.metadata_key, &iv_bytes);
let mut metadata_bytes: [u8; METADATA_LEN] = [0; METADATA_LEN];
for i in 0..METADATA_LEN {
metadata_bytes[i] = chacha_block[i] ^ encrypted_metadata_bytes[i];
}

(iv_bytes, metadata_bytes)
}

// Errors if the payment preimage doesn't match `payment_hash`. Returns the bad preimage bytes in
// this case.
fn derive_ldk_payment_preimage(payment_hash: PaymentHash, iv_bytes: &[u8; IV_LEN], metadata_bytes: &[u8; METADATA_LEN], keys: &ExpandedKey) -> Result<PaymentPreimage, [u8; 32]> {
let mut hmac = HmacEngine::<Sha256>::new(&keys.ldk_pmt_hash_key);
hmac.input(iv_bytes);
hmac.input(metadata_bytes);
let decoded_payment_preimage = Hmac::from_engine(hmac).into_inner();
if !fixed_time_eq(&payment_hash.0, &Sha256::hash(&decoded_payment_preimage).into_inner()) {
return Err(decoded_payment_preimage);
}
return Ok(PaymentPreimage(decoded_payment_preimage))
}

fn hkdf_extract_expand(salt: &[u8], ikm: &KeyMaterial) -> ExpandedKey {
let mut hmac = HmacEngine::<Sha256>::new(salt);
hmac.input(&ikm.0);
Expand Down Expand Up @@ -5026,8 +5062,9 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
/// payment secret fetched via this method or [`create_inbound_payment`], and which is at least
/// the `min_value_msat` provided here, if one is provided.
///
/// The [`PaymentHash`] (and corresponding [`PaymentPreimage`]) must be globally unique. This
/// method may return an Err if another payment with the same payment_hash is still pending.
/// The [`PaymentHash`] (and corresponding [`PaymentPreimage`]) should be globally unique, though
/// note that LDK will not stop you from registering duplicate payment hashes for inbound
/// payments.
///
/// `min_value_msat` should be set if the invoice being generated contains a value. Any payment
/// received for the returned [`PaymentHash`] will be required to be at least `min_value_msat`
Expand Down Expand Up @@ -5079,6 +5116,14 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
self.set_payment_hash_secret_map(payment_hash, None, min_value_msat, invoice_expiry_delta_secs)
}

/// Gets an LDK-generated payment preimage from a payment hash and payment secret that were
/// previously returned from [`create_inbound_payment`].
///
/// [`create_inbound_payment`]: Self::create_inbound_payment
pub fn get_payment_preimage(&self, payment_hash: PaymentHash, payment_secret: PaymentSecret) -> Result<PaymentPreimage, APIError> {
inbound_payment::get_payment_preimage(payment_hash, payment_secret, &self.inbound_payment_key)
}

#[cfg(any(test, feature = "fuzztarget", feature = "_test_utils"))]
pub fn get_and_clear_pending_events(&self) -> Vec<events::Event> {
let events = core::cell::RefCell::new(Vec::new());
Expand Down
34 changes: 33 additions & 1 deletion lightning/src/ln/payment_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ use chain::{ChannelMonitorUpdateErr, Confirm, Listen, Watch};
use chain::channelmonitor::{ANTI_REORG_DELAY, ChannelMonitor, LATENCY_GRACE_PERIOD_BLOCKS};
use chain::transaction::OutPoint;
use ln::channelmanager::{BREAKDOWN_TIMEOUT, ChannelManager, ChannelManagerReadArgs, PaymentId, PaymentSendFailure};
use ln::features::InitFeatures;
use ln::features::{InitFeatures, InvoiceFeatures};
use ln::msgs;
use ln::msgs::ChannelMessageHandler;
use routing::router::{Payee, get_route};
use util::events::{ClosureReason, Event, MessageSendEvent, MessageSendEventsProvider};
use util::test_utils;
use util::errors::APIError;
Expand Down Expand Up @@ -706,3 +707,34 @@ fn test_fulfill_restart_failure() {
// it had already considered the payment fulfilled, and now they just got free money.
assert!(nodes[0].node.get_and_clear_pending_events().is_empty());
}

#[test]
fn get_ldk_payment_preimage() {
// Ensure that `ChannelManager::get_payment_preimage` can successfully be used to claim a payment.
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs);
create_announced_chan_between_nodes(&nodes, 0, 1, InitFeatures::known(), InitFeatures::known());

let amt_msat = 60_000;
let expiry_secs = 60 * 60;
let (payment_hash, payment_secret) = nodes[1].node.create_inbound_payment(Some(amt_msat), expiry_secs).unwrap();

let payee = Payee::from_node_id(nodes[1].node.get_our_node_id())
.with_features(InvoiceFeatures::known());
let scorer = test_utils::TestScorer::with_fixed_penalty(0);
let route = get_route(
&nodes[0].node.get_our_node_id(), &payee, &nodes[0].network_graph,
Some(&nodes[0].node.list_usable_channels().iter().collect::<Vec<_>>()),
amt_msat, TEST_FINAL_CLTV, nodes[0].logger, &scorer).unwrap();
let _payment_id = nodes[0].node.send_payment(&route, payment_hash, &Some(payment_secret)).unwrap();
check_added_monitors!(nodes[0], 1);

// Make sure to use `get_payment_preimage`
let payment_preimage = nodes[1].node.get_payment_preimage(payment_hash, payment_secret).unwrap();
let mut events = nodes[0].node.get_and_clear_pending_msg_events();
assert_eq!(events.len(), 1);
pass_along_path(&nodes[0], &[&nodes[1]], amt_msat, payment_hash, Some(payment_secret), events.pop().unwrap(), true, Some(payment_preimage));
claim_payment_along_route(&nodes[0], &[&[&nodes[1]]], false, payment_preimage);
}