-
Notifications
You must be signed in to change notification settings - Fork 386
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
Peer Storage Feature – Part 2 #3623
base: main
Are you sure you want to change the base?
Peer Storage Feature – Part 2 #3623
Conversation
d8df7cf
to
361738d
Compare
Seems this isn't properly rebased, hence CI is failing? |
361738d
to
a1006e0
Compare
I think #3626 will fix this. |
No, it fails with:
|
That is because it’s trying to compile the commented code in the documentation, I will fix it in a bit. |
ff7fec7
to
465da4a
Compare
4f29813
to
2113034
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.
Unfortunately CI is still failing as the code doesn't work in no_std
.
lightning/src/ln/our_peer_storage.rs
Outdated
use crate::ln::msgs::DecodeError; | ||
|
||
/// [`OurPeerStorage`] is used to store channel information that allows for the creation of a | ||
/// PeerStorage backup. It includes versioning and timestamping for comparison between |
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: IIRC, we settled on snake_case here?
/// PeerStorage backup. It includes versioning and timestamping for comparison between | |
/// `peer_storage` backup. It includes versioning and timestamping for comparison between |
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.
Sorry, my bad. Fixed!
lightning/src/ln/our_peer_storage.rs
Outdated
impl OurPeerStorage { | ||
/// Returns a [`OurPeerStorage`] with version 1 and current timestamp. | ||
pub fn new() -> Self { | ||
let duration_since_epoch = std::time::SystemTime::now() |
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.
As LDK also supports no_std
, we need to make this code work in no_std
environments. As accessing time is only available in std
, it means we either need to find a way to have the user provide the timestamp
themselves, or feature-gate the peer storage to only work on std
.
That said, are we positive that we need a timestamp in the first place?
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.
No, I have added a timestamp so that if we receive multiple peer storage instances, we can select the latest one. I was thinking of replacing the timestamp with block height...
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.
Not sure why we need to select the latest one specifically? Can't we just decode all of them and see if any have new data for our ChannelMonitor
s?
Thanks for the review @tnull. Fixed the CI :) |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3623 +/- ##
==========================================
+ Coverage 89.24% 89.90% +0.66%
==========================================
Files 155 156 +1
Lines 119280 124273 +4993
Branches 119280 124273 +4993
==========================================
+ Hits 106448 111733 +5285
+ Misses 10238 9982 -256
+ Partials 2594 2558 -36 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
lightning/src/sign/mod.rs
Outdated
/// This function derives an encryption key for peer storage by using the HKDF | ||
/// (HMAC-based Key Derivation Function) with a specific label and the node | ||
/// secret key. The derived key is used for encrypting or decrypting peer storage | ||
/// data. | ||
/// | ||
/// The process involves the following steps: | ||
/// 1. Retrieves the node secret key. | ||
/// 2. Uses the node secret key and the label `"Peer Storage Encryption Key"` | ||
/// to perform HKDF extraction and expansion. | ||
/// 3. Returns the first part of the derived key, which is a 32-byte array. |
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.
No, this is a trait method, it doesn't, itself, do anything. Documentation on a trait method should describe what the method should do as well as additional context for when the method is called and possibly a possible implementation, but it what the method "does" do.
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, makes sense. Fixed it.
lightning/src/sign/mod.rs
Outdated
@@ -2201,6 +2230,14 @@ impl NodeSigner for KeysManager { | |||
self.inbound_payment_key.clone() | |||
} | |||
|
|||
fn get_peer_storage_key(&self) -> [u8; 32] { | |||
let (t1, _) = hkdf_extract_expand_twice( |
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.
Rather than HKDF'ing from the node secret, let's derive a new key from the next hardened idx off the seed in KeysManager::new
?
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.
Sure, I will update the PR to derive the key from the next hardened index. Could you share a bit more about the reasoning behind this approach?
lightning/src/ln/our_peer_storage.rs
Outdated
/// # Fields | ||
/// - `version`: Defines the structure's version for backward compatibility. | ||
/// - `timestamp`: UNIX timestamp indicating the creation or modification time of the instance. | ||
/// - `ser_channels`: Serialized channel data. |
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 weird to document private things?
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 initially implemented this with public fields for simplicity, but after further consideration, I refactored the code to use getter and setter methods instead.
lightning/src/ln/our_peer_storage.rs
Outdated
impl OurPeerStorage { | ||
/// Returns a [`OurPeerStorage`] with version 1 and current timestamp. | ||
pub fn new() -> Self { | ||
let duration_since_epoch = std::time::SystemTime::now() |
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.
Not sure why we need to select the latest one specifically? Can't we just decode all of them and see if any have new data for our ChannelMonitor
s?
lightning/src/ln/our_peer_storage.rs
Outdated
} | ||
} | ||
|
||
/// Stubs a channel inside [`OurPeerStorage`] |
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 have no idea from the docs in this module what it means to "stub a channel" nor what this method does without looking at the code. Maybe just say that it "sets the serialized data to be included in the storage"?
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.
Sorry for being less descriptive. Fixed it.
lightning/src/ln/our_peer_storage.rs
Outdated
|
||
impl OurPeerStorage { | ||
/// Returns a [`OurPeerStorage`] with version 1 and current timestamp. | ||
pub fn new() -> Self { |
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 flow is fairly brittle. We currently have to create a dummy OurPeerStorage
with new
, then store the data we need with stub_channels
after calling encrypt_our_peer_storage
on the data. But at no point does any of this change the type. Instead, why not just have a method to create_from_data
that takes the key and does the encryption, and a decrypt(&self) -> Vec<u8>
data that returns the data decrypted?
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.
Agreed. In the future PR we might need setter for ser_channels
but not now. Thanks!
lightning/src/ln/msgs.rs
Outdated
/// This trait extends [`MessageSendEventsProvider`], meaning it is capable of generating | ||
/// message send events, which can be processed using | ||
/// [`MessageSendEventsProvider::get_and_clear_pending_msg_events`]. | ||
pub trait SendingOnlyMessageHandler: MessageSendEventsProvider { |
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.
Its not clear to me why this needs a trait? MessageSendEventsProvider
already lets us send messages, and send_peer_storage
is only called internally in chainmonitor.rs
so it doesn't seem like something we need to expose to the world (let along via a trait rather than letting ChainMonitor
have the method directly)
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, that's what I initially thought as well. However, since we need to process events through PeerManager::process_events
, I considered adding a new message handler to allow calling get_and_clear_pending_msg_events
on ChainMonitor
.
What could be an alternative structure that would still allow access to ChainMonitor::get_and_clear_pending_msg_events
through PeerManager::process_events
?
@@ -388,7 +392,7 @@ where C::Target: chain::Filter, | |||
/// pre-filter blocks or only fetch blocks matching a compact filter. Otherwise, clients may | |||
/// always need to fetch full blocks absent another means for determining which blocks contain | |||
/// transactions relevant to the watched channels. | |||
pub fn new(chain_source: Option<C>, broadcaster: T, logger: L, feeest: F, persister: P) -> Self { | |||
pub fn new(chain_source: Option<C>, broadcaster: T, logger: L, feeest: F, persister: P, our_peerstorage_encryption_key: [u8; 32]) -> Self { |
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 should explicitly document that this should be from the KeysManager
's method to match what ChannelManager
does.
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.
Thanks for pointing this out. Fixed!
lightning/src/chain/chainmonitor.rs
Outdated
@@ -255,6 +257,8 @@ pub struct ChainMonitor<ChannelSigner: EcdsaChannelSigner, C: Deref, T: Deref, F | |||
/// it to give to users (or [`MonitorEvent`]s for `ChannelManager` to process). | |||
event_notifier: Notifier, | |||
pending_send_only_events: Mutex<Vec<MessageSendEvent>>, | |||
our_peer_storage: Mutex<OurPeerStorage>, |
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.
Not sure I understand the point of storing this in the ChainMonitor
all the time. Rather, isn't it simpler to just build the OurPeerStorage
that we need when send_peer_storage
is called, encrypt it, and directly enqueue it as a message?
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.
Agreed. Thanks for pointing out.
lightning/src/ln/channelmanager.rs
Outdated
let mut res = vec![0; msg.data.len() - 16]; | ||
let our_peerstorage_encryption_key = self.node_signer.get_peer_storage_key(); | ||
let mut cyphertext_with_key = Vec::with_capacity(msg.data.len() + our_peerstorage_encryption_key.len()); | ||
cyphertext_with_key.extend(msg.data.clone()); |
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.
Please pass the key and ciphertext as separate arguments rather than concatenating them.
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.
Implemented this change based on the feedback from #2943 (comment).
This unfortunately needs a rebase now. |
Add get_peer_storage_key method to derive a 32-byte encryption key for securing Peer Storage. This method utilizes HKDF with the node's secret key as input and a fixed info string to generate the encryption key. - Add 'get_peer_storage_key' to NodeSigner. - Implement 'get_peer_storage_key' for KeysManager & PhantomKeysManager.
Introduce the OurPeerStorage struct to manage serialized channel data for peer storage backups. This struct facilitates the distribution of peer storage to channel partners and includes versioning and timestamping for comparison between retrieved peer storage instances. - Add the OurPeerStorage struct with fields for version, timestamp, and serialized channel data (ser_channels). - Implement methods to encrypt and decrypt peer storage securely. - Add functionality to update channel data within OurPeerStorage.
62d2026
to
22fe4b2
Compare
To enable ChainMonitor sending peer storage to channel partners whenever a new block is added, We implement BaseMessageHandler for ChainMonitor. This allows the `ChainMonitor` to handle the peer storage distribution. Key changes: - Add BaseMessageHandler into the MessageHandler. - Implement BaseMessageHandler for ChainMonitor. - Process BaseMessageHandler events inside process_events().
Everytime a new block is added we send PeerStorage to all of our channel partner. - Add our_peer_storage and our_peerstorage_encryption_key to ChainMonitor - Write send_peer_storage() and send it to all channel partners whenever a new block is added
Ensure ChannelManager properly handles peer_storage_retrieval. - Write internal_peer_storage_retreival to verify if we recv correct peer storage. - Send error if we get invalid peer_storage data.
Ensure that we correctly handle the sendpeerstorage message event from chainmonitor and process it through channelmonitor. Key Changes: - Retrieve sendpeerstorage message event from chainmonitor for both nodes. - Handle peer storage messages exchanged between nodes and verify correct decryption.
22fe4b2
to
35863ce
Compare
@@ -616,12 +628,6 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) { | |||
Arc::new(TestPersister { update_ret: Mutex::new(ChannelMonitorUpdateStatus::Completed) }), | |||
)); | |||
|
|||
let keys_manager = Arc::new(KeyProvider { |
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.
Not quite sure why this code move is necessary?
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.
So that we can use keys_manager.get_peer_storage_key()
inside ChainMonitor::new()
.
lightning/src/sign/mod.rs
Outdated
/// | ||
/// Implementations of this method must derive a secure encryption key using a | ||
/// cryptographically strong key derivation function (e.g., HKDF or a hardened | ||
/// child key derivation). The derived key is used to encrypt or decrypt peer |
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.
Could link BIP32 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.
Yes, Thanks!
/// This method is invoked when encrypting or decrypting peer storage data. | ||
/// It must return the same key every time it is called, ensuring consistency | ||
/// for encryption and decryption operations. | ||
fn get_peer_storage_key(&self) -> [u8; 32]; |
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.
Why does this return a [u8; 32]
rather than simply a SecretKey
?
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.
Agreed, fixing this...
lightning/src/ln/our_peer_storage.rs
Outdated
/// and decryption to ensure data integrity and security during exchange or storage. | ||
/// | ||
/// # Key Methods | ||
/// - `create_from_data`: Returns an encrypted PeerStorage instance created from the provided data. |
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: Tick/link PeerStorage
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.
Fixed
lightning/src/ln/our_peer_storage.rs
Outdated
|
||
impl OurPeerStorage { | ||
/// Get `ser_channels` field from [`OurPeerStorage`] | ||
pub fn get_ser_channels(&self) -> Vec<u8> { |
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: According to the Rust API guidlines, this should just be called ser_channels
. Also, maybe it would be worth spelling/renaming out ser_channels
as the name doesn't communicate super clearly what it holds.
Also, can we have this return a reference and leave it up to the callsite whether they want/need to clone or not?
lightning/src/ln/channelmanager.rs
Outdated
let logger = WithContext::from(&self.logger, Some(counterparty_node_id), None, None); | ||
|
||
log_debug!(logger, "Received unexpected peer_storage_retrieval from {}. This is unusual since we do not yet distribute peer storage. Sending a warning.", log_pubkey!(counterparty_node_id)); | ||
if msg.data.len() < 16 { |
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 are these 16 bytes exactly? Please introduce a clearly named constant for this.
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, it's min_cyphertext_length because of the auth tag length. I will name it accordingly.
lightning/src/ln/channelmanager.rs
Outdated
let mut res = vec![0; msg.data.len() - 16]; | ||
let our_peerstorage_encryption_key = self.node_signer.get_peer_storage_key(); | ||
|
||
match OurPeerStorage::decrypt_our_peer_storage(&mut res, &msg.data.clone(), our_peerstorage_encryption_key) { |
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 should be able to avoid this clone if we have decrypt_our_peer_storage
take the argument by value.
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, thanks for pointing this out.
lightning/src/ln/channelmanager.rs
Outdated
), ChannelId([0; 32]))); | ||
} | ||
} | ||
let our_peer_storage = <OurPeerStorage as Readable>::read(&mut ::bitcoin::io::Cursor::new(res)).unwrap(); |
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 make me wonder if it wouldn't be cleaner to introduce an EncryptedPeerStorage
(or similar) helper type that we can transition to/from rather than doing all these conversions 'explictly'.
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 should make the flow smoother. Modifying decrypt_our_peer_storage
to directly return OurPeerStorage
.
lightning/src/ln/channelmanager.rs
Outdated
"Invalid peer_storage_retrieval message received.".into(), | ||
), ChannelId([0; 32]))) | ||
if our_peer_storage.get_ser_channels().len() == 0 { | ||
log_debug!(logger, "Received a peer storage from peer {} with 0 channels.", log_pubkey!(counterparty_node_id)); |
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.
Should we generally log (in TRACE?) when we successfully decrypted a backup, not only when it's length 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.
Currently we just distribute empty peer storages, in the next PR we are going to serialise channelmonitors and send them over.
@@ -1919,6 +1919,7 @@ fn test_trampoline_inbound_payment_decoding() { | |||
fn sign_invoice( | |||
&self, _invoice: &RawBolt11Invoice, _recipient: Recipient, | |||
) -> Result<RecoverableSignature, ()> { unreachable!() } | |||
fn get_peer_storage_key(&self) -> [u8;32] { unreachable!() } |
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: Formatting is off.
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.
Fixed.
Thank you so much, @tnull and @TheBlueMatt, for taking the time to review this. I’ve done my best to address all the comments. Please let me know if there’s anything I might have missed or if further changes are needed. |
|
||
if msg.data.len() < MIN_CYPHERTEXT_LEN { | ||
log_debug!(logger, "Invalid YourPeerStorage received from {}", log_pubkey!(counterparty_node_id)); | ||
return Err(MsgHandleErrInternal::from_chan_no_close(ChannelError::Warn( |
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.
Not sure we need to warn our peer if they send us bogus data.
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 won’t lead to a unilateral close and is also mentioned in the spec.
It's not compulsory, but it won’t make much of a difference, I guess?
The receiver of peer_storage_retrieval:
when it receives peer_storage_retrieval with an outdated or irrelevant data:
MAY send a warning.
lightning/src/ln/our_peer_storage.rs
Outdated
} | ||
} | ||
|
||
impl Writeable for OurPeerStorage { |
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 dont need to implement readable/writeable now since we use just a vec for the encrypted data, no?
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.
Writeable is used internally inside create_from_data
, and Readable is used inside decrypt_our_peer_storage
. I agree we currently don’t need them very much, but in the next one, we are going to use them for serialisation and deserialisation. Wouldn’t it be better to have it in this one instead of implementing serialisation and deserialisation logic directly within those functions?
lightning/src/sign/mod.rs
Outdated
/// This method is invoked when encrypting or decrypting peer storage data. | ||
/// It must return the same key every time it is called, ensuring consistency | ||
/// for encryption and decryption operations. | ||
fn get_peer_storage_key(&self) -> SecretKey; |
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 don't think this needs to be a SecretKey
? SecretKey
implies secp256k1 operations. We really want a [u8; 32]
, 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.
Done!
lightning/src/sign/mod.rs
Outdated
/// - The derived key must be exactly **32 bytes** and suitable for symmetric | ||
/// encryption algorithms. |
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 is given by the return type, we don't have to specify it.
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 wrote it just as a note to implementations. But, yeah, the return type specifies it. Removing it.
lightning/src/sign/mod.rs
Outdated
/// | ||
/// # Returns | ||
/// | ||
/// A [`SecretKey`] representing the encryption key for peer storage. |
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 does this communicate that the function signature does not?
lightning/src/sign/mod.rs
Outdated
/// | ||
/// # Implementation Details | ||
/// | ||
/// - The key must be derived from a node-specific secret to ensure uniqueness. |
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'm not sure what a "node-specific secret" means. Honestly, I'm not sure any of the sections below the first paragraph are needed.
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, on second thought, I agree it’s unnecessary.
Co-authored-by: Matt Corallo <649246+TheBlueMatt@users.noreply.github.com>
Co-authored-by: Matt Corallo <649246+TheBlueMatt@users.noreply.github.com>
10d7a4d
to
4adeeee
Compare
This is the second PR in the peer storage feature series.
Key Changes
In the next one, I will add serialisation logic for ChannelMonitors inside peer storage.