-
Notifications
You must be signed in to change notification settings - Fork 375
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
Keysend #967
Keysend #967
Conversation
9c80f5f
to
93a3533
Compare
Codecov Report
@@ Coverage Diff @@
## main #967 +/- ##
==========================================
- Coverage 90.78% 90.75% -0.04%
==========================================
Files 60 60
Lines 30926 31322 +396
==========================================
+ Hits 28076 28426 +350
- Misses 2850 2896 +46
Continue to review full report at Codecov.
|
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.
Concept ACK the (public) api changes.
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.
Concept ACK
a4fe071
to
b825069
Compare
b825069
to
de9d7f7
Compare
cf23fbf
to
08a1cb3
Compare
2cdf5fb
to
a003460
Compare
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.
Structure all looks good IMO, one request for more tests and a comment or two.
lightning/src/ln/channelmanager.rs
Outdated
impl_writeable_tlv_based!(ClaimableHTLC, { | ||
(0, prev_hop, required), | ||
(2, value, required), | ||
(4, payment_data, required), | ||
// XXX what to do here? | ||
(4, claim_type, required), |
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.
Hmm, right, so to make this compatible we need to pull the impl_writeable_tlv_based
apart and implement manually - if ClaimableType
is Invoice
we'll write the 4-type payment_data
and if its Spontaneous
we'll write an 8-type preimage. Its not ideal and we could just cheat here if we want, but IMO its not a ton of work here.
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.
I think I did this
af5ea24
to
85950c0
Compare
if let Some(final_data) = payment_data { | ||
if final_data.total_msat > MAX_VALUE_MSAT { panic!("We should never be sending infinite/overflow onion payments"); } | ||
} | ||
encode_varint_length_prefixed_tlv!(w, { | ||
(2, HighZeroBytesDroppedVarInt(self.amt_to_forward), required), | ||
(4, HighZeroBytesDroppedVarInt(self.outgoing_cltv_value), required), | ||
(8, payment_data, option) | ||
(8, payment_data, option), | ||
(5482373484, keysend_preimage, option) |
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.
nit: could you use a trailing comma so the blame layer for future additions don't affect this line?
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.
The macro gives me an error when I do this :/
(2, payment_secret, required), | ||
(2, payment_secret, option), |
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.
Just to check my understanding, going from required to optional is ok, but going from optional to required would necessitate increasing the version number?
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.
I think there's ways to make both directions work depending on if you're willing to add a special case in the code for backwards-compatibility? @TheBlueMatt would have to confirm
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.
I think going from option to required is never allowed. If we move from option to required, and an old version "wrote" None
, we'll fail to deserialize data written by an old node, which is not allowed unless we are deliberately breaking reading old versions (presumably after several sequential versions that always wrote Some
so that you have to be reading very old data to fail).
(8, payment_preimage, option), | ||
}); | ||
let purpose = match payment_secret { |
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.
If payment_secret
were optional in InvoicePayment
, would we be able to distinguish what the purpose is? In other words, should we encode this as an enum instead? Or would that break compatibility?
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.
In other words, should we encode this as an enum instead?
Could you clarify roughly what enum is being suggested? Didn't quite follow..
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.
PaymentPurpose
enum using impl_writeable_tlv_based_enum
.
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.
Believe that'd break compatibility
lightning/src/ln/channelmanager.rs
Outdated
impl Readable for ClaimableHTLC { | ||
fn read<R: Read>(reader: &mut R) -> Result<Self, DecodeError> { | ||
init_tlv_field_var!(prev_hop, required); | ||
init_tlv_field_var!(value, required); | ||
init_tlv_field_var!(payment_data, required); | ||
init_tlv_field_var!(cltv_expiry, required); | ||
init_tlv_field_var!(keysend_preimage, option); | ||
read_tlv_fields! | ||
(reader, | ||
{ | ||
(0, prev_hop, required), (2, value, required), | ||
(4, payment_data, required), (6, cltv_expiry, required), | ||
(8, keysend_preimage, option) | ||
}); | ||
let claim_type = match keysend_preimage { | ||
Some(p) => { | ||
let data: msgs::FinalOnionHopData = payment_data.0.unwrap(); | ||
if data.payment_secret != PaymentSecret([0; 32]) || data.total_msat != 0 { | ||
return Err(DecodeError::InvalidValue) | ||
} | ||
ClaimableType::Spontaneous(p) | ||
}, | ||
None => ClaimableType::Invoice(payment_data.0.unwrap()), | ||
}; | ||
Ok(Self { | ||
prev_hop: init_tlv_based_struct_field!(prev_hop, required), | ||
value: init_tlv_based_struct_field!(value, required), | ||
claim_type, | ||
cltv_expiry: init_tlv_based_struct_field!(cltv_expiry, required), | ||
}) | ||
} | ||
} |
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.
Seems like as we change our data format more, the more complex our (de)serialization logic becomes as we can no longer use impl_writeable_tlv_based
. I'm wondering if it would be any better to bump the version but support reading older versions. Then maybe you could condition on the version read and use a macro for implementing each version we'd like to support? I might be missing something about this, though.
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.
Hmm, I dont think bumping the version helps a ton, then we'd just have two copies of the deserialization logic - one for the old objects, and one for the new, and we'd have to switch between them based on the version. That just results in a code blowup that's almost equivalent, I think.
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.
I guess then the versions would at least be fairly well delineated. Otherwise, if we refactor further there is more cognitive overhead in figuring out the various cases. But maybe at that point we'd just decided to bump versions. I don't feel strongly on this, but just want to point out the cost.
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.
That said, if our version is only in topic-level structs (is it?) then we'd have to pass it through. So maybe not very feasible / clean?
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.
Right, I have a feeling sooner or later we'll adapt our read trait to pass a version number. I haven't bothered yet, but it may be required eventually. I agree we should be careful about growing bloat in read functions, maybe it makes sense to have a "we only support things written by versions as old as one year ago"-style policy.
lightning/src/ln/channelmanager.rs
Outdated
impl_writeable_tlv_based!(ClaimableHTLC, { | ||
(0, prev_hop, required), | ||
(2, value, required), | ||
(4, payment_data, required), | ||
(6, cltv_expiry, required), | ||
}); | ||
impl Writeable for ClaimableHTLC { | ||
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ::std::io::Error> { | ||
let payment_data = match &self.claim_type { | ||
ClaimableType::Invoice(data) => data.clone(), | ||
_ => msgs::FinalOnionHopData { | ||
payment_secret: PaymentSecret([0; 32]), | ||
total_msat: 0, | ||
} | ||
}; | ||
let keysend_preimage = match self.claim_type { | ||
ClaimableType::Invoice(_) => None, | ||
ClaimableType::Spontaneous(preimage) => Some(preimage.clone()), | ||
}; | ||
write_tlv_fields! | ||
(writer, | ||
{ | ||
(0, self.prev_hop, required), (2, self.value, required), | ||
(4, payment_data, required), (6, self.cltv_expiry, required), | ||
(8, keysend_preimage, option), | ||
}); | ||
Ok(()) | ||
} |
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.
Do we need to do this because we introduced an enum where there wasn't one before? Would it make sense to bump the version and serialize enums using the macro for doing so?
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.
Yeah: #967 (comment)
Would it make sense to bump the version and serialize enums using the macro for doing so?
Is this the same idea as in this comment? I'm pretty eager to get this PR out the door to unblock sphinx, but would love to get @TheBlueMatt's input on this idea since it does sound potentially nicer
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.
Yeah, similar concern.
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.
We can't "just bump the version" since we want to at least try (I think) to be compatible where old versions can read new serialized data. Because some users will be synchronizing state across multiple applications that may upgrade at different times, I think we should generally try to at least have our data be readable by the previous version as long as you aren't using new features.
(2, payment_secret, required), | ||
(2, payment_secret, option), |
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.
I think going from option to required is never allowed. If we move from option to required, and an old version "wrote" None
, we'll fail to deserialize data written by an old node, which is not allowed unless we are deliberately breaking reading old versions (presumably after several sequential versions that always wrote Some
so that you have to be reading very old data to fail).
85950c0
to
0ec10bd
Compare
lightning/src/ln/channelmanager.rs
Outdated
// final_expiry_too_soon | ||
// We have to have some headroom to broadcast on chain if we have the preimage, so make sure we have at least | ||
// HTLC_FAIL_BACK_BUFFER blocks to go. | ||
// Also, ensure that, in the case of an unknown payment hash, our payment logic has enough time to fail the HTLC backward |
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.
I think this comment could be clearer saying "an unknown preimage for the received payment hash" ?
I don't think this is true that we trigger a channel closure for an inbound HTLC of which the payment hash is unknown. See ChannelMonitor::should_broadcast_holder_commitment_txn
, if the htlc is !htlc_outbound
and we don't know a self.payment_preimages.contains_key(&htlc.payment_hash)
so it doesn't matter for our payment logic has enough time to fail the HTLC backward ?
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.
I think this comment could be clearer saying "an unknown preimage for the received payment hash" ?
Added this!
@@ -1863,6 +1886,23 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana | |||
} | |||
} | |||
|
|||
/// Send a spontaneous payment, which is a payment that does not require the recipient to have |
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.
What if the recipient did generate an invoice with a given payment hash and signals even payment_secret
feature bit ? If the recipient is a LDK node, I think it'll fail the keysend reception as we don't allow payment_secret + keysend_preimage ?
Maybe a bit edgy but point to be cleared up with keysend specification ? Like invoice reception implies MUST NOT pay with keysend
?
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.
Right, I think the behavior here is right, we should expect a "real" payment, the spec should mention this.
}); | ||
}, | ||
hash_map::Entry::Occupied(_) => { | ||
log_trace!(self.logger, "Failing new keysend HTLC with payment_hash {} for a duplicative payment hash", log_bytes!(payment_hash.0)); |
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.
IIUC this case handling, if we've a valid (payment_secret, payment_hash) pair in self.pending_inbound_payments
and we receive a latter keysend with the same hash, we fail the keysend instead of claiming ?
If you're an intermediary hop Mallory and you relay HTLC 1 hash X to final hop Bob, I think this let Mallory send a "final-receiver probing" keysend hash X to Bob and observe the early failure (PendingHTLCsForwardable
vs PaymentReceived
processing times diff) ? If this is correct, I'm not sure if it binds well the requirement laid out above ("Further, we must not expose whether we have any other HTLCs associated with the same payment_hash pending or not')...
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.
Grr good catch. Going one step up the stack, I wonder if we're gonna have users screw this up reliably too. If you're a user and doing accounting based on the payment_hash
when you issue invoices, if you receive a keysend I can see users looking up the payment hash in their accounting database. I wonder if we should avoid exposing the keysend payment hash/preimage entirely. I guess some users may want them for other reasons, but we should go to great pains to point out in documentation that you MUST NEVER EVER co-mingle payment hash lookups between keysend and non-keysend. Maybe at least we should name the field differently to call it "keysend preimage" instead of "payment preimage"?
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.
Can we avoid to return payment hash to avoid mangle with the accounting database ? I can foresee users willingly to know the preimage if it has any semantic sense like a pay-to-publish, privacy-preserving satoshi place. But within the keysend model I would say the payment hash should be ignored by the receiver there is no hash validation operation to realize against a database.
And if the receiver wants to prove delivery they can still manually hash the preimage. We can even return a prefixed preimage by concatening a "Lightning keysend" ?
You might even have worst collision, where a low-value keysend trigger the release of a virtual/physical good by your business logic, substituting to a regular high-value payment...?
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.
Right, disregard, I had made that comment before I saw your suggestion at #967 (comment)
@@ -2304,15 +2380,27 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana | |||
let mut total_value = 0; | |||
let htlcs = channel_state.claimable_htlcs.entry(payment_hash) | |||
.or_insert(Vec::new()); | |||
if htlcs.len() == 1 { | |||
if let OnionPayload::Spontaneous(_) = htlcs[0].onion_payload { | |||
log_trace!(self.logger, "Failing new HTLC with payment_hash {} as we already had an existing keysend HTLC with the same payment hash", log_bytes!(payment_hash.0)); |
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.
Well I've a doubt if this doesn't introduce a weird payment censorship attack. Let's say an attacker observe that a payment invoice hash X has been issued by Alice. From now on, it will repeatedly send keysend X to Alice aiming to provoke a collision with honest HTLC X, thus triggering a reject.
Is the concern exposed sound ? I don't think the attacker even has to be on the honest HTLC payment path...
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.
Yea, but its solved by #967 (comment) as well, no?
incoming_cltv_expiry: msg.cltv_expiry, | ||
} | ||
} else if let Some(payment_preimage) = keysend_preimage { | ||
PendingHTLCRouting::ReceiveKeysend { |
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.
What do you think about checking early that keysend preimage == HTLC hash and isn't pure junk, returning 0x4000|22 if it is ? May help with probing concerns expressed after.
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.
Ah! Yea, if we do that then we don't have to completely rewrite this whole PR to avoid the issues you noted at #967 (comment)
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.
Yes i think that's okay to solve the issues. I had a concern with MPP payment where a preimage revealed could be replay as a probing keysend as long as the whole eMPP isn't settled but in fact we won't start the MPP settlement phase until all chunks have been received. And when we do so we remove the entry from pending_inbound_payments
and as such the collision ?
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.
I think it doesn't matter either way - basically we would be using the payment preimage the same way we use the payment secret today - to authenticate that the sender of a given HTLC is the one authorized to send for that payment hash. We reliably check this authentication immediately upon HTLC receipt both for MPP and regular payments, and will just have to also do it for keysend.
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.
What do you think about checking early that keysend preimage == HTLC hash and isn't pure junk, returning 0x4000|22 if it is ? May help with probing concerns expressed after.
Added this check! Thanks for the thoughtful review Antoine
0ec10bd
to
b6d7d55
Compare
log_trace!(self.logger, "Failing new HTLC with payment_hash {} as we didn't have a corresponding inbound payment.", log_bytes!(payment_hash.0)); | ||
fail_htlc!(claimable_htlc); | ||
}, | ||
OnionPayload::Spontaneous(preimage) => { |
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.
Just to be clear, this doesn't prevent honest keysend replay, where a buggy/hazardous payer sends twice a keysend with the same hash, even with different payment paths, and as such opens the payment to be claimed in-flight by an intermediary node ? Note, I think this is already a risk for any hashlock-based LN payment (variant of wormhole attacks...)
I would say we even care less that w.r.t to regular payments as we keysend the preimage doesn't have the proof-of-payment property plead most of the time ?
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.
Hmm, yes it does seem like keysend requires more thought than normal payments for replay situations. I don't think it would open up buggy clients to in-flight claims by an intermediate node though, because wouldn't the buggy client run into issues once they started "replaying" the whole update_add
dance, preventing the HTLC from being successfully added to the commitment tx to begin with?
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.
I mean the HTLC would appear under a new htlc_id
. I'm thinking a situation similar to duplicate_htlc_test
/test_duplicate_htlc_different_direction_onchain
. Though nevermind, we don't prevent double-spend already for normal payments, see comment around send_payment
.
b6d7d55
to
d724a68
Compare
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.
See my points about payment fault-injection and the lowered cost of hash-collision in our ChannelMonitor
.
I wonder if we shouldn't introduce a no_keysend_receive
config setting. Previously, we have a cleaner separation between routing and payment endpoint, and a routing hop no eager to receive payments could have just not issue invoices at all. I think this is blurred by keysend, and you might have weird interactions to care about with potential higher application logic. E.g a third-party replaying hash trying to provoke undefined behaviors. Likely going to be worst if we implement MPP keysend.
I think we can address this config setting question in a follow-up, otherwise I believe the PR to be pretty mature. Great work overall!
}, | ||
/// Because this is a spontaneous payment, the payer generated their own preimage rather than us | ||
/// (the payee) providing a preimage. | ||
SpontaneousPayment(PaymentPreimage), |
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.
I think we're bouncing between mentions of "spontaenous" or "keysend" payments across this PR to designate the same feature. LDK-user wise, maybe we should bind to a consistent naming ?
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.
@jkczyz I was thinking that people might want to specifically send either a keysend spontaneous payment or a non-keysend spontaneous payment if they know that the payee only supports one or the other. So, I'm somewhat in favor of switching all Spontaneous
variable names to Keysend
(including s/send_spontaneous_payment/send_keysend_payment
). Thoughts?
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.
My understanding was that Keysend is a specific type of spontaneous payment that is the predecessor to spontaneous AMP payments. They both seem either unintuitive or somewhat overloaded names and also are implementation details from a user's perspective.
On the receiving side (Event::PaymentReceived
), does the user need to know what implementation was used to make the spontaneous payment? If not, I'd argue we shouldn't expose the implementation detail in our public API.
On the sending side (ChannelManager
), how does the user know if the receiver accepts spontaneous payments and using which implementation? If via feature flags, could we chose which implementation to use based on the flags rather than making the user choose?
On the processing side, can these be generalized in any way?
I don't have definitive answers to these, but I'd imagine we could at least keep the first two generic (i.e., "spontaneous") and if needed make the third specific to the implementation.
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.
w/r/t:
On the receiving side (Event:: PaymentReceived), does the user need to know what implementation was used to make the spontaneous payment?
If via feature flags, could we chose which implementation to use based on the flags rather than making the user choose?
Not sure, pinged Sphinx for their thoughts last night. Hopefully they get back to me soonish
how does the user know if the receiver accepts spontaneous payments and using which implementation?
It's a little annoying right now... C-Lightning and Eclair advertise feature bit 55 if they support keysend, but iiuc lnd doesn't advertise any feature bits for keysend. So, right now you know if the receiver doesn't support keysend if they fail back the payment, but there's no good way to tell if they do support keysend.
if let OnionPayload::Invoice(ref data) = claimable_htlc.onion_payload { | ||
data.clone() | ||
} else { | ||
log_trace!(self.logger, "Failing new keysend HTLC with payment_hash {} because we already have an inbound payment with the same payment hash", log_bytes!(payment_hash.0)); |
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.
Do you have concerns of fault-injection attacks against honest routing hops introduced with our keysend implementation ?
Let's say you have the following topology : A -> B -> C -> D -> E.
A is the payer, it draws a payment path to the payee E, going through B, C and D.
B receives a keysend X and from HTLC traffic A's habits assumes it's a keysend.
B knows a payment secret for C, which is also a merchant node and deliver on-demand.
B sends a regular payment X to C then A's keysend X.
C receives in-order the regular payment X then A's keysend X, provoking the failure of the keysend.
C returns an error to A as the source of the payment path failure.
A downgrades C's routing reputation and doesn't select it for future payment paths.
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.
C returns an error to A as the source of the payment path failure.
C Should only fail if the payment is to C. It should (maybe could use a test for this) still happily forward an HTLC with the same payment hash onwards.
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.
Ah you're right, the relayed keysend payment shouldn't appear twice in pending_inbound_payments
we won't generate a PaymentReceived
.
// we have at least HTLC_FAIL_BACK_BUFFER blocks to go. | ||
// Also, ensure that, in the case of an unknown preimage for the received payment hash, our | ||
// payment logic has enough time to fail the HTLC backward before our onchain logic triggers a | ||
// channel closure (see HTLC_FAIL_BACK_BUFFER rationale). |
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.
I think keysend make it a bit easier to exploit our hash-collision in ChannelMonitor.payment_preimages
.
Let's say you have the following topology : M1 -- A -- B -- M2
M1 and M2 are collusioning attackers.
M1 sends a keysend to A, populating its payment_preimages
with H (ChannelManager::claim_funds_from_hop()
)
M1 sends a regular payment H to M2 via A and B
M2 breaks the chan, and pin the commitment, B will never update_fail_htlc
to A before the forward payment is settled (is_resolving_htlc_output
)
B goes onchain on the AB link as there is a pending inbound HTLC for which it knows the preimage.
M1 and M2 succeed to close a channel, of which they're not a counterparty
Attack can be made economically rational by batching, if B has neighboors C, D, E, ..., malicious HTLCs on their channels can be routed and made pending on the one M2's commitment
Previously, this attack would have require interactivity with B, like paying an invoice for each malicious HTLC
Ultimately, I don't think keysend is faultive, it's a known defect of our ChannelMonitor logic, so I don't believe there is anything to do on this PR...
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.
it's a known defect of our ChannelMonitor logic
If the attack starts with "M2 prevents a channel-close transaction from going onchain because of pinning" I don't think we have a bug, lightning has a bug :). There isn't anything we can do about this short of fixing Bitcoin Core.
e008dad
to
4fdcda5
Compare
Needs rebase. |
This doesn't yet use the field, but it will be used in upcoming commits.
C-Lightning requires us to advertise this feature before they'll attempt a keysend payment to us.
This will allow keysend tests to assert that the PaymentReceived payment preimage is as expected in upcoming commits.
and use it to make more keysend tests
Clearer phrasing
4fdcda5
to
6dd6289
Compare
Code Review ACK 6dd6289, good on my side! Thanks to adding coverage and can't think of of other weirdness. W.r.t to naming we can fix it with follow-ups once we have feedbacks from users. |
Thanks, I agree, we can bikeshed on naming in follow-up |
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.
Looks good mod one comment. Gonna go ahead and merge so we can move forward, but would be good to follow up.
/// never reach the recipient. | ||
/// | ||
/// Similar to regular payments, you MUST NOT reuse a `payment_preimage` value. See | ||
/// [`send_payment`] for more information about the risks of duplicate preimage usage. |
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.
This also needs to mention that you should see send_payment
for more details on the return value. Further, this needs to mention that the Route
must only have at most one path. Finally, we need to actually check that, but not here - as far as I can see we don't currently require that non-payment-secret-containing sends don't contain more than one path, which we should do in send_payment_internal
.
SGTM |
Based on #964
06/23/2021: Seeking concept ACKs for the API before adding tests