XXX: Track SCID aliases from our counterparty and use them in invoices
authorMatt Corallo <git@bluematt.me>
Wed, 1 Sep 2021 16:48:35 +0000 (16:48 +0000)
committerMatt Corallo <git@bluematt.me>
Tue, 21 Dec 2021 00:16:55 +0000 (00:16 +0000)
New `funding_locked` messages can include SCID aliases which our
counterparty will recognize as "ours" for the purposes of relaying
transactions to us. This avoids telling the world about our
on-chain transactions every time we want to receive a payment, and
will allow for receiving payments before the funding transaction
appears on-chain.

Here we store the new SCID aliases and use them in invoices instead
of he "standard" SCIDs.

XXX: Note in spec pr that we need to drop the "MUST NOT retransmit funding_locked"
message in reconnection if we are passed initial reconnect. This may
imply a feature bit, but will at least imply some kind of "MUST NOT
retransmit if it doesn't have evidence the counterparty interpreted the
SCID alias previously sent" (to avoid backwards incompat).

lightning-invoice/src/utils.rs
lightning/src/ln/channel.rs
lightning/src/ln/channelmanager.rs
lightning/src/routing/router.rs

index 53fdc13f2fc2255f7102a3225252f4139e887efc..f23faff9e3ea47c3f1662fc897baf447bbec6ac1 100644 (file)
@@ -39,9 +39,11 @@ where
        let our_channels = channelmanager.list_usable_channels();
        let mut route_hints = vec![];
        for channel in our_channels {
-               let short_channel_id = match channel.short_channel_id {
-                       Some(id) => id,
-                       None => continue,
+               let short_channel_id = if let Some(scid) = channel.inbound_scid_alias { scid } else {
+                       match channel.short_channel_id {
+                               Some(id) => id,
+                               None => continue,
+                       }
                };
                let forwarding_info = match channel.counterparty.forwarding_info {
                        Some(info) => info,
index 60e162d5a4cfc473ca84eda4d0069bde4e91f5c0..c6e365c5d56d608171f39d44e3bb534d8f4b2ed9 100644 (file)
@@ -655,6 +655,13 @@ pub(super) struct Channel<Signer: Sign> {
 
        /// This channel's type, as negotiated during channel open
        channel_type: ChannelTypeFeatures,
+
+       // Our counerparty can offer us SCID aliases which they will map to this channel when routing
+       // outbound payments. These can be used in invoice route hints to provide privacy of which
+       // on-chain transaction is ours.
+       // We only bother storing the most recent SCID alias at any time, though our counterparty has
+       // to store all of them.
+       latest_inbound_scid_alias: Option<u64>,
 }
 
 #[cfg(any(test, feature = "fuzztarget"))]
@@ -900,6 +907,8 @@ impl<Signer: Sign> Channel<Signer> {
 
                        workaround_lnd_bug_4006: None,
 
+                       latest_inbound_scid_alias: None,
+
                        #[cfg(any(test, feature = "fuzztarget"))]
                        historical_inbound_htlc_fulfills: HashSet::new(),
 
@@ -1200,6 +1209,8 @@ impl<Signer: Sign> Channel<Signer> {
 
                        workaround_lnd_bug_4006: None,
 
+                       latest_inbound_scid_alias: None,
+
                        #[cfg(any(test, feature = "fuzztarget"))]
                        historical_inbound_htlc_fulfills: HashSet::new(),
 
@@ -2068,6 +2079,15 @@ impl<Signer: Sign> Channel<Signer> {
                        return Err(ChannelError::Ignore("Peer sent funding_locked when we needed a channel_reestablish. The peer is likely lnd, see https://github.com/lightningnetwork/lnd/issues/4006".to_owned()));
                }
 
+               if let Some(scid_alias) = msg.short_channel_id_alias {
+                       if Some(scid_alias.0) != self.short_channel_id {
+                               // The scid alias provided can be used to route payments *from* our counterparty,
+                               // i.e. can be used for inbound payments and provided in invoices, but is not used
+                               // whrn routing outbound payments.
+                               self.latest_inbound_scid_alias = Some(scid_alias.0);
+                       }
+               }
+
                let non_shutdown_state = self.channel_state & (!MULTI_STATE_FLAGS);
 
                if non_shutdown_state == ChannelState::FundingSent as u32 {
@@ -2075,17 +2095,28 @@ impl<Signer: Sign> Channel<Signer> {
                } else if non_shutdown_state == (ChannelState::FundingSent as u32 | ChannelState::OurFundingLocked as u32) {
                        self.channel_state = ChannelState::ChannelFunded as u32 | (self.channel_state & MULTI_STATE_FLAGS);
                        self.update_time_counter += 1;
-               } else if (self.channel_state & (ChannelState::ChannelFunded as u32) != 0 &&
-                                // Note that funding_signed/funding_created will have decremented both by 1!
-                                self.cur_holder_commitment_transaction_number == INITIAL_COMMITMENT_NUMBER - 1 &&
-                                self.cur_counterparty_commitment_transaction_number == INITIAL_COMMITMENT_NUMBER - 1) ||
-                               // If we reconnected before sending our funding locked they may still resend theirs:
-                               (self.channel_state & (ChannelState::FundingSent as u32 | ChannelState::TheirFundingLocked as u32) ==
-                                                     (ChannelState::FundingSent as u32 | ChannelState::TheirFundingLocked as u32)) {
-                       if self.counterparty_cur_commitment_point != Some(msg.next_per_commitment_point) {
+               } else if self.channel_state & (ChannelState::ChannelFunded as u32) != 0 ||
+                       // If we reconnected before sending our funding locked they may still resend theirs:
+                       (self.channel_state & (ChannelState::FundingSent as u32 | ChannelState::TheirFundingLocked as u32) ==
+                                             (ChannelState::FundingSent as u32 | ChannelState::TheirFundingLocked as u32))
+               {
+                       // They probably disconnected/reconnected and re-sent the funding_locked, which is
+                       // required, or we're getting a fresh SCID alias.
+                       let expected_point =
+                               if self.cur_counterparty_commitment_transaction_number == INITIAL_COMMITMENT_NUMBER - 1 {
+                                       // If they haven't ever sent an updated point, the point they send should match
+                                       // the current one.
+                                       self.counterparty_cur_commitment_point
+                               } else {
+                                       // If they have sent updated points, funding_locked is always supposed to match
+                                       // their "first" point, which we re-derive here.
+                                       self.commitment_secrets.get_secret(INITIAL_COMMITMENT_NUMBER - 1)
+                                               .map(|secret| SecretKey::from_slice(&secret).ok()).flatten()
+                                               .map(|sk| PublicKey::from_secret_key(&self.secp_ctx, &sk))
+                               };
+                       if expected_point != Some(msg.next_per_commitment_point) {
                                return Err(ChannelError::Close("Peer sent a reconnect funding_locked with a different point".to_owned()));
                        }
-                       // They probably disconnected/reconnected and re-sent the funding_locked, which is required
                        return Ok(None);
                } else {
                        return Err(ChannelError::Close("Peer sent a funding_locked at a strange time".to_owned()));
@@ -4102,6 +4133,11 @@ impl<Signer: Sign> Channel<Signer> {
                self.short_channel_id
        }
 
+       /// Allowed in any state (including after shutdown)
+       pub fn get_latest_inbound_scid_alias(&self) -> Option<u64> {
+               self.latest_inbound_scid_alias
+       }
+
        /// Returns the funding_txo we either got from our peer, or were given by
        /// get_outbound_funding_created.
        pub fn get_funding_txo(&self) -> Option<OutPoint> {
@@ -5632,6 +5668,7 @@ impl<Signer: Sign> Writeable for Channel<Signer> {
                        (11, self.monitor_pending_finalized_fulfills, vec_type),
                        (13, self.channel_creation_height, required),
                        (15, self.announcement_sigs_state, required),
+                       (17, self.latest_inbound_scid_alias, option),
                });
 
                Ok(())
@@ -5876,6 +5913,7 @@ impl<'a, Signer: Sign, K: Deref> ReadableArgs<(&'a K, u32)> for Channel<Signer>
                // If we read an old Channel, for simplicity we just treat it as "we never sent an
                // AnnouncementSignatures" which implies we'll re-send it on reconnect, but that's fine.
                let mut announcement_sigs_state = Some(AnnouncementSigsState::NotSent);
+               let mut latest_inbound_scid_alias = None;
                read_tlv_fields!(reader, {
                        (0, announcement_sigs, option),
                        (1, minimum_depth, option),
@@ -5889,6 +5927,7 @@ impl<'a, Signer: Sign, K: Deref> ReadableArgs<(&'a K, u32)> for Channel<Signer>
                        (11, monitor_pending_finalized_fulfills, vec_type),
                        (13, channel_creation_height, option),
                        (15, announcement_sigs_state, option),
+                       (17, latest_inbound_scid_alias, option),
                });
 
                let chan_features = channel_type.as_ref().unwrap();
@@ -5997,6 +6036,8 @@ impl<'a, Signer: Sign, K: Deref> ReadableArgs<(&'a K, u32)> for Channel<Signer>
 
                        workaround_lnd_bug_4006: None,
 
+                       latest_inbound_scid_alias,
+
                        #[cfg(any(test, feature = "fuzztarget"))]
                        historical_inbound_htlc_fulfills,
 
index 3aa5c7b2d6ca0c2c377128b86fd62c5899dbcf14..b1ea5b2ea8222ee5cfd65545c2a97ef079eb2a54 100644 (file)
@@ -1188,6 +1188,11 @@ pub struct ChannelDetails {
        /// The position of the funding transaction in the chain. None if the funding transaction has
        /// not yet been confirmed and the channel fully opened.
        pub short_channel_id: Option<u64>,
+       /// An optional [`short_channel_id`] alias for this channel, randomly generated by our
+       /// counterparty and usable in place of [`short_channel_id`] in invoice route hints. Our
+       /// counterparty will recognize the alias provided here in place of the [`short_channel_id`]
+       /// when they see a payment to be routed to us.
+       pub inbound_scid_alias: Option<u64>,
        /// The value, in satoshis, of this channel as appears in the funding output
        pub channel_value_satoshis: u64,
        /// The value, in satoshis, that must always be held in the channel for us. This value ensures
@@ -1812,6 +1817,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
                                        },
                                        funding_txo: channel.get_funding_txo(),
                                        short_channel_id: channel.get_short_channel_id(),
+                                       inbound_scid_alias: channel.get_latest_inbound_scid_alias(),
                                        channel_value_satoshis: channel.get_value_satoshis(),
                                        unspendable_punishment_reserve: to_self_reserve_satoshis,
                                        balance_msat,
index e49844383bbceee091e9d42b7262416109b2d484..02ea58596890abae85e32efe5e22433df92845be 100644 (file)
@@ -1519,6 +1519,7 @@ mod tests {
                        },
                        funding_txo: Some(OutPoint { txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(), index: 0 }),
                        short_channel_id,
+                       inbound_scid_alias: None,
                        channel_value_satoshis: 0,
                        user_channel_id: 0,
                        balance_msat: 0,