-
Notifications
You must be signed in to change notification settings - Fork 407
Add support for phantom node payments #1199
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
Changes from all commits
de1b62e
f254bb4
329ecdf
4706c75
f6c75d8
adeec71
e1c33d4
70f7db9
410eb05
c417a51
710954f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,9 +8,9 @@ use bitcoin_hashes::Hash; | |
use crate::prelude::*; | ||
use lightning::chain; | ||
use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator}; | ||
use lightning::chain::keysinterface::{Sign, KeysInterface}; | ||
use lightning::chain::keysinterface::{Recipient, KeysInterface, Sign}; | ||
use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; | ||
use lightning::ln::channelmanager::{ChannelDetails, ChannelManager, PaymentId, PaymentSendFailure, MIN_FINAL_CLTV_EXPIRY}; | ||
use lightning::ln::channelmanager::{ChannelDetails, ChannelManager, PaymentId, PaymentSendFailure, PhantomRouteHints, MIN_FINAL_CLTV_EXPIRY, MIN_CLTV_EXPIRY_DELTA}; | ||
use lightning::ln::msgs::LightningError; | ||
use lightning::routing::scoring::Score; | ||
use lightning::routing::network_graph::{NetworkGraph, RoutingFees}; | ||
|
@@ -21,6 +21,99 @@ use core::convert::TryInto; | |
use core::ops::Deref; | ||
use core::time::Duration; | ||
|
||
#[cfg(feature = "std")] | ||
/// Utility to create an invoice that can be paid to one of multiple nodes, or a "phantom invoice." | ||
/// See [`PhantomKeysManager`] for more information on phantom node payments. | ||
/// | ||
/// `phantom_route_hints` parameter: | ||
/// * Contains channel info for all nodes participating in the phantom invoice | ||
valentinewallace marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// * Entries are retrieved from a call to [`ChannelManager::get_phantom_route_hints`] on each | ||
/// participating node | ||
/// * It is fine to cache `phantom_route_hints` and reuse it across invoices, as long as the data is | ||
/// updated when a channel becomes disabled or closes | ||
/// * Note that if too many channels are included in [`PhantomRouteHints::channels`], the invoice | ||
/// may be too long for QR code scanning. To fix this, `PhantomRouteHints::channels` may be pared | ||
/// down | ||
/// | ||
/// `payment_hash` and `payment_secret` come from [`ChannelManager::create_inbound_payment`] or | ||
/// [`ChannelManager::create_inbound_payment_for_hash`]. These values can be retrieved from any | ||
/// participating node. | ||
Comment on lines
+38
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'd move us further away from #1295 |
||
/// | ||
/// Note that the provided `keys_manager`'s `KeysInterface` implementation must support phantom | ||
/// invoices in its `sign_invoice` implementation ([`PhantomKeysManager`] satisfies this | ||
/// requirement). | ||
/// | ||
/// [`PhantomKeysManager`]: lightning::chain::keysinterface::PhantomKeysManager | ||
/// [`ChannelManager::get_phantom_route_hints`]: lightning::ln::channelmanager::ChannelManager::get_phantom_route_hints | ||
/// [`PhantomRouteHints::channels`]: lightning::ln::channelmanager::PhantomRouteHints::channels | ||
pub fn create_phantom_invoice<Signer: Sign, K: Deref>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does it interferes with MPP ? Am I correct that you can't MPP shard to each one of the participating nodes as they won't communicate among themselves shard reception ? Maybe we could do that in the future, if we move the MPP accounting logic from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yep!
I suggested this to @TheBlueMatt but it seems to be too much of a compromise on safety guarantees, e.g. it could allow the user to cause us to release the preimage too early. Also a bit of a layer violation. Adding a comment clarifying this! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think that's okay on the safety guarantees, if you have some two-phases commit protocol (you announce the HTLC reception to the coordinating server and then when all are received you release the preimage). IIRC, we might need such protocol if we want to coordinate well the channel monitor replicas. If it does happen, we can re-think MPP phantom invoice in the future. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean its definitely possible to provide a complicated two-phase API for this, but I'm not sure that means we should. It'd definitely be a complicated API, and if a user wants to receive a large payment they can also just generate non-phantom invoices for large payments. We can revisit this if someone really needs it, though, indeed. |
||
amt_msat: Option<u64>, description: String, payment_hash: PaymentHash, payment_secret: | ||
PaymentSecret, phantom_route_hints: Vec<PhantomRouteHints>, keys_manager: K, network: Currency | ||
) -> Result<Invoice, SignOrCreationError<()>> where K::Target: KeysInterface { | ||
if phantom_route_hints.len() == 0 { | ||
return Err(SignOrCreationError::CreationError(CreationError::MissingRouteHints)) | ||
} | ||
let mut invoice = InvoiceBuilder::new(network) | ||
.description(description) | ||
.current_timestamp() | ||
.payment_hash(Hash::from_slice(&payment_hash.0).unwrap()) | ||
.payment_secret(payment_secret) | ||
.min_final_cltv_expiry(MIN_FINAL_CLTV_EXPIRY.into()); | ||
valentinewallace marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if let Some(amt) = amt_msat { | ||
invoice = invoice.amount_milli_satoshis(amt); | ||
} | ||
|
||
for hint in phantom_route_hints { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possibly would make these more difficult to filter, but I wonder if we should just have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, nevermind. That would allow someone to call |
||
for channel in &hint.channels { | ||
let short_channel_id = match channel.short_channel_id { | ||
Some(id) => id, | ||
None => continue, | ||
}; | ||
let forwarding_info = match &channel.counterparty.forwarding_info { | ||
Some(info) => info.clone(), | ||
None => continue, | ||
}; | ||
invoice = invoice.private_route(RouteHint(vec![ | ||
RouteHintHop { | ||
src_node_id: channel.counterparty.node_id, | ||
short_channel_id, | ||
fees: RoutingFees { | ||
base_msat: forwarding_info.fee_base_msat, | ||
proportional_millionths: forwarding_info.fee_proportional_millionths, | ||
}, | ||
cltv_expiry_delta: forwarding_info.cltv_expiry_delta, | ||
htlc_minimum_msat: None, | ||
htlc_maximum_msat: None, | ||
}, | ||
RouteHintHop { | ||
src_node_id: hint.real_node_pubkey, | ||
short_channel_id: hint.phantom_scid, | ||
fees: RoutingFees { | ||
base_msat: 0, | ||
proportional_millionths: 0, | ||
}, | ||
cltv_expiry_delta: MIN_CLTV_EXPIRY_DELTA, | ||
htlc_minimum_msat: None, | ||
htlc_maximum_msat: None, | ||
}]) | ||
); | ||
} | ||
} | ||
|
||
let raw_invoice = match invoice.build_raw() { | ||
Ok(inv) => inv, | ||
Err(e) => return Err(SignOrCreationError::CreationError(e)) | ||
}; | ||
let hrp_str = raw_invoice.hrp.to_string(); | ||
let hrp_bytes = hrp_str.as_bytes(); | ||
let data_without_signature = raw_invoice.data.to_base32(); | ||
let signed_raw_invoice = raw_invoice.sign(|_| keys_manager.sign_invoice(hrp_bytes, &data_without_signature, Recipient::PhantomNode)); | ||
match signed_raw_invoice { | ||
Ok(inv) => Ok(Invoice::from_signed(inv).unwrap()), | ||
Err(e) => Err(SignOrCreationError::SignError(e)) | ||
} | ||
} | ||
|
||
#[cfg(feature = "std")] | ||
/// Utility to construct an invoice. Generally, unless you want to do something like a custom | ||
/// cltv_expiry, this is what you should be using to create an invoice. The reason being, this | ||
|
@@ -118,7 +211,7 @@ where | |
let hrp_str = raw_invoice.hrp.to_string(); | ||
let hrp_bytes = hrp_str.as_bytes(); | ||
let data_without_signature = raw_invoice.data.to_base32(); | ||
let signed_raw_invoice = raw_invoice.sign(|_| keys_manager.sign_invoice(hrp_bytes, &data_without_signature)); | ||
let signed_raw_invoice = raw_invoice.sign(|_| keys_manager.sign_invoice(hrp_bytes, &data_without_signature, Recipient::Node)); | ||
match signed_raw_invoice { | ||
Ok(inv) => Ok(Invoice::from_signed(inv).unwrap()), | ||
Err(e) => Err(SignOrCreationError::SignError(e)) | ||
|
@@ -192,13 +285,17 @@ where | |
mod test { | ||
use core::time::Duration; | ||
use {Currency, Description, InvoiceDescription}; | ||
use lightning::ln::PaymentHash; | ||
use bitcoin_hashes::Hash; | ||
use bitcoin_hashes::sha256::Hash as Sha256; | ||
use lightning::chain::keysinterface::PhantomKeysManager; | ||
use lightning::ln::{PaymentPreimage, PaymentHash}; | ||
use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY; | ||
use lightning::ln::functional_test_utils::*; | ||
use lightning::ln::features::InitFeatures; | ||
use lightning::ln::msgs::ChannelMessageHandler; | ||
use lightning::routing::router::{PaymentParameters, RouteParameters, find_route}; | ||
use lightning::util::events::MessageSendEventsProvider; | ||
use lightning::util::enforcing_trait_impls::EnforcingSigner; | ||
use lightning::util::events::{MessageSendEvent, MessageSendEventsProvider, Event}; | ||
use lightning::util::test_utils; | ||
use utils::create_invoice_from_channelmanager_and_duration_since_epoch; | ||
|
||
|
@@ -254,4 +351,121 @@ mod test { | |
let events = nodes[1].node.get_and_clear_pending_msg_events(); | ||
assert_eq!(events.len(), 2); | ||
} | ||
|
||
#[test] | ||
#[cfg(feature = "std")] | ||
fn test_multi_node_receive() { | ||
do_test_multi_node_receive(true); | ||
do_test_multi_node_receive(false); | ||
} | ||
|
||
#[cfg(feature = "std")] | ||
fn do_test_multi_node_receive(user_generated_pmt_hash: bool) { | ||
let mut chanmon_cfgs = create_chanmon_cfgs(3); | ||
let seed_1 = [42 as u8; 32]; | ||
let seed_2 = [43 as u8; 32]; | ||
let cross_node_seed = [44 as u8; 32]; | ||
chanmon_cfgs[1].keys_manager.backing = PhantomKeysManager::new(&seed_1, 43, 44, &cross_node_seed); | ||
chanmon_cfgs[2].keys_manager.backing = PhantomKeysManager::new(&seed_2, 43, 44, &cross_node_seed); | ||
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); | ||
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); | ||
let nodes = create_network(3, &node_cfgs, &node_chanmgrs); | ||
let chan_0_1 = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100000, 10001, InitFeatures::known(), InitFeatures::known()); | ||
nodes[0].node.handle_channel_update(&nodes[1].node.get_our_node_id(), &chan_0_1.1); | ||
nodes[1].node.handle_channel_update(&nodes[0].node.get_our_node_id(), &chan_0_1.0); | ||
let chan_0_2 = create_announced_chan_between_nodes_with_value(&nodes, 0, 2, 100000, 10001, InitFeatures::known(), InitFeatures::known()); | ||
nodes[0].node.handle_channel_update(&nodes[2].node.get_our_node_id(), &chan_0_2.1); | ||
nodes[2].node.handle_channel_update(&nodes[0].node.get_our_node_id(), &chan_0_2.0); | ||
|
||
let payment_amt = 10_000; | ||
let (payment_preimage, payment_hash, payment_secret) = { | ||
if user_generated_pmt_hash { | ||
let payment_preimage = PaymentPreimage([1; 32]); | ||
let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).into_inner()); | ||
let payment_secret = nodes[1].node.create_inbound_payment_for_hash(payment_hash, Some(payment_amt), 3600).unwrap(); | ||
(payment_preimage, payment_hash, payment_secret) | ||
} else { | ||
let (payment_hash, payment_secret) = nodes[1].node.create_inbound_payment(Some(payment_amt), 3600).unwrap(); | ||
let payment_preimage = nodes[1].node.get_payment_preimage(payment_hash, payment_secret).unwrap(); | ||
(payment_preimage, payment_hash, payment_secret) | ||
} | ||
}; | ||
let route_hints = vec![ | ||
nodes[1].node.get_phantom_route_hints(), | ||
nodes[2].node.get_phantom_route_hints(), | ||
]; | ||
let invoice = ::utils::create_phantom_invoice::<EnforcingSigner, &test_utils::TestKeysInterface>(Some(payment_amt), "test".to_string(), payment_hash, payment_secret, route_hints, &nodes[1].keys_manager, Currency::BitcoinTestnet).unwrap(); | ||
valentinewallace marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
assert_eq!(invoice.min_final_cltv_expiry(), MIN_FINAL_CLTV_EXPIRY as u64); | ||
assert_eq!(invoice.description(), InvoiceDescription::Direct(&Description("test".to_string()))); | ||
assert_eq!(invoice.route_hints().len(), 2); | ||
assert!(!invoice.features().unwrap().supports_basic_mpp()); | ||
|
||
let payment_params = PaymentParameters::from_node_id(invoice.recover_payee_pub_key()) | ||
.with_features(invoice.features().unwrap().clone()) | ||
.with_route_hints(invoice.route_hints()); | ||
let params = RouteParameters { | ||
payment_params, | ||
final_value_msat: invoice.amount_milli_satoshis().unwrap(), | ||
final_cltv_expiry_delta: invoice.min_final_cltv_expiry() as u32, | ||
}; | ||
let first_hops = nodes[0].node.list_usable_channels(); | ||
let network_graph = node_cfgs[0].network_graph; | ||
let logger = test_utils::TestLogger::new(); | ||
let scorer = test_utils::TestScorer::with_penalty(0); | ||
let route = find_route( | ||
&nodes[0].node.get_our_node_id(), ¶ms, network_graph, | ||
Some(&first_hops.iter().collect::<Vec<_>>()), &logger, &scorer, | ||
).unwrap(); | ||
let (payment_event, fwd_idx) = { | ||
let mut payment_hash = PaymentHash([0; 32]); | ||
payment_hash.0.copy_from_slice(&invoice.payment_hash().as_ref()[0..32]); | ||
nodes[0].node.send_payment(&route, payment_hash, &Some(invoice.payment_secret().clone())).unwrap(); | ||
let mut added_monitors = nodes[0].chain_monitor.added_monitors.lock().unwrap(); | ||
assert_eq!(added_monitors.len(), 1); | ||
added_monitors.clear(); | ||
|
||
let mut events = nodes[0].node.get_and_clear_pending_msg_events(); | ||
assert_eq!(events.len(), 1); | ||
let fwd_idx = match events[0] { | ||
MessageSendEvent::UpdateHTLCs { node_id, .. } => { | ||
if node_id == nodes[1].node.get_our_node_id() { | ||
1 | ||
} else { 2 } | ||
}, | ||
_ => panic!("Unexpected event") | ||
}; | ||
(SendEvent::from_event(events.remove(0)), fwd_idx) | ||
}; | ||
nodes[fwd_idx].node.handle_update_add_htlc(&nodes[0].node.get_our_node_id(), &payment_event.msgs[0]); | ||
commitment_signed_dance!(nodes[fwd_idx], nodes[0], &payment_event.commitment_msg, false, true); | ||
|
||
// Note that we have to "forward pending HTLCs" twice before we see the PaymentReceived as | ||
// this "emulates" the payment taking two hops, providing some privacy to make phantom node | ||
// payments "look real" by taking more time. | ||
expect_pending_htlcs_forwardable_ignore!(nodes[fwd_idx]); | ||
nodes[fwd_idx].node.process_pending_htlc_forwards(); | ||
expect_pending_htlcs_forwardable_ignore!(nodes[fwd_idx]); | ||
nodes[fwd_idx].node.process_pending_htlc_forwards(); | ||
|
||
let payment_preimage_opt = if user_generated_pmt_hash { None } else { Some(payment_preimage) }; | ||
expect_payment_received!(&nodes[fwd_idx], payment_hash, payment_secret, payment_amt, payment_preimage_opt); | ||
do_claim_payment_along_route(&nodes[0], &vec!(&vec!(&nodes[fwd_idx])[..]), false, payment_preimage); | ||
let events = nodes[0].node.get_and_clear_pending_events(); | ||
assert_eq!(events.len(), 2); | ||
match events[0] { | ||
Event::PaymentSent { payment_preimage: ref ev_preimage, payment_hash: ref ev_hash, ref fee_paid_msat, .. } => { | ||
assert_eq!(payment_preimage, *ev_preimage); | ||
assert_eq!(payment_hash, *ev_hash); | ||
assert_eq!(fee_paid_msat, &Some(0)); | ||
}, | ||
_ => panic!("Unexpected event") | ||
} | ||
match events[1] { | ||
Event::PaymentPathSuccessful { payment_hash: hash, .. } => { | ||
assert_eq!(hash, Some(payment_hash)); | ||
}, | ||
_ => panic!("Unexpected event") | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A comment here would be useful for posterity.