Skip to content
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

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

adi2011
Copy link

@adi2011 adi2011 commented Feb 26, 2025

This is the second PR in the peer storage feature series.

Key Changes

  • Enable ChainMonitor to send peer storage to all channel partners whenever a new block is mined.
  • Add get_peer_storage_key() to NodeSigner.
  • Implement decryption logic for peer storage in handle_peer_storage_retrieval().

In the next one, I will add serialisation logic for ChannelMonitors inside peer storage.

@adi2011 adi2011 force-pushed the peer-storage/encrypt-decrypt branch 3 times, most recently from d8df7cf to 361738d Compare February 27, 2025 03:31
@adi2011 adi2011 marked this pull request as ready for review February 27, 2025 07:46
@tnull
Copy link
Contributor

tnull commented Feb 27, 2025

Seems this isn't properly rebased, hence CI is failing?

@adi2011 adi2011 force-pushed the peer-storage/encrypt-decrypt branch from 361738d to a1006e0 Compare February 27, 2025 11:25
@adi2011
Copy link
Author

adi2011 commented Feb 27, 2025

Seems this isn't properly rebased, hence CI is failing?

I think #3626 will fix this.

@tnull
Copy link
Contributor

tnull commented Feb 27, 2025

Seems this isn't properly rebased, hence CI is failing?

I think #3626 will fix this.

No, it fails with:

src/ln/our_peer_storage.rs - ln::our_peer_storage::OurPeerStorage (line 48) stdout ----
error[E0433]: failed to resolve: use of undeclared type `OurPeerStorage`
 --> src/ln/our_peer_storage.rs:49:28
  |
3 | let mut our_peer_storage = OurPeerStorage::new();
  |                            ^^^^^^^^^^^^^^ not found in this scope
  |
help: consider importing this struct
  |
2 | use lightning::ln::our_peer_storage::OurPeerStorage;
  |

error[E0433]: failed to resolve: use of undeclared type `OurPeerStorage`
 --> src/ln/our_peer_storage.rs:54:1
  |
8 | OurPeerStorage::decrypt_our_peer_storage(&mut decrypted, &encrypted).unwrap();
  | ^^^^^^^^^^^^^^ not found in this scope
  |
help: consider importing this struct
  |
2 | use lightning::ln::our_peer_storage::OurPeerStorage;
  |

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0433`.
Couldn't compile the test.

failures:
    src/ln/our_peer_storage.rs - ln::our_peer_storage::OurPeerStorage (line 48)

test result: FAILED. 28 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.00s

error: test failed, to rerun pass '--doc'

@adi2011
Copy link
Author

adi2011 commented Feb 27, 2025

That is because it’s trying to compile the commented code in the documentation, I will fix it in a bit.

@adi2011 adi2011 force-pushed the peer-storage/encrypt-decrypt branch 2 times, most recently from ff7fec7 to 465da4a Compare February 27, 2025 12:09
@jkczyz jkczyz added the weekly goal Someone wants to land this this week label Feb 27, 2025
@adi2011 adi2011 force-pushed the peer-storage/encrypt-decrypt branch 2 times, most recently from 4f29813 to 2113034 Compare February 28, 2025 04:51
Copy link
Contributor

@tnull tnull left a 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.

impl OurPeerStorage {
/// Returns a [`OurPeerStorage`] with version 1 and current timestamp.
pub fn new() -> Self {
let duration_since_epoch = std::time::SystemTime::now()
Copy link
Contributor

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?

Copy link
Author

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...

Copy link
Collaborator

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 ChannelMonitors?

@adi2011 adi2011 requested a review from tnull March 1, 2025 07:08
@adi2011
Copy link
Author

adi2011 commented Mar 1, 2025

Thanks for the review @tnull.

Fixed the CI :)

Copy link

codecov bot commented Mar 1, 2025

Codecov Report

Attention: Patch coverage is 83.91960% with 32 lines in your changes missing coverage. Please review.

Project coverage is 89.37%. Comparing base (c355ea4) to head (7d56ef7).
Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/channelmanager.rs 74.00% 12 Missing and 1 partial ⚠️
lightning/src/chain/chainmonitor.rs 80.95% 8 Missing ⚠️
lightning/src/ln/peer_handler.rs 60.00% 5 Missing and 1 partial ⚠️
lightning/src/ln/our_peer_storage.rs 95.55% 2 Missing ⚠️
lightning/src/util/test_utils.rs 66.66% 2 Missing ⚠️
lightning/src/ln/blinded_payment_tests.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3623      +/-   ##
==========================================
+ Coverage   89.20%   89.37%   +0.16%     
==========================================
  Files         155      156       +1     
  Lines      119377   121060    +1683     
  Branches   119377   121060    +1683     
==========================================
+ Hits       106496   108193    +1697     
+ Misses      10266    10262       -4     
+ Partials     2615     2605      -10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines 861 to 870
/// 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.
Copy link
Collaborator

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.

Copy link
Author

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.

@@ -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(
Copy link
Collaborator

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?

Copy link
Author

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?

Comment on lines 30 to 33
/// # 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.
Copy link
Collaborator

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?

Copy link
Author

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.

impl OurPeerStorage {
/// Returns a [`OurPeerStorage`] with version 1 and current timestamp.
pub fn new() -> Self {
let duration_since_epoch = std::time::SystemTime::now()
Copy link
Collaborator

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 ChannelMonitors?

}
}

/// Stubs a channel inside [`OurPeerStorage`]
Copy link
Collaborator

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"?

Copy link
Author

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.


impl OurPeerStorage {
/// Returns a [`OurPeerStorage`] with version 1 and current timestamp.
pub fn new() -> Self {
Copy link
Collaborator

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?

Copy link
Author

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!

/// 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 {
Copy link
Collaborator

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)

Copy link
Author

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 {
Copy link
Collaborator

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.

Copy link
Author

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!

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, if it's ~a requirement to use the key retrieved via NodeSigner::get_peer_storage_key, why not actually introduce a PeerStorageKey (new)type rather than using a generic [u8; 32] that could be anything?

Copy link
Author

Choose a reason for hiding this comment

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

I agree PeerStorageKey would be better than [u8; 32], what do you think @TheBlueMatt?

@@ -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>,
Copy link
Collaborator

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?

Copy link
Author

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.

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());
Copy link
Collaborator

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.

Copy link
Author

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).

@adi2011 adi2011 requested a review from TheBlueMatt March 6, 2025 17:49
@tnull
Copy link
Contributor

tnull commented Mar 7, 2025

This unfortunately needs a rebase now.

@adi2011 adi2011 force-pushed the peer-storage/encrypt-decrypt branch 2 times, most recently from 22fe4b2 to 35863ce Compare March 7, 2025 21:41

impl OurPeerStorage {
/// Get `ser_channels` field from [`OurPeerStorage`]
pub fn get_ser_channels(&self) -> Vec<u8> {
Copy link
Contributor

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?

@adi2011
Copy link
Author

adi2011 commented Mar 11, 2025

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(
Copy link
Collaborator

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.

Copy link
Author

@adi2011 adi2011 Mar 11, 2025

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.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess it doesn't matter much, but if a peer is corrupting our data they probably know it cause they're corrupting state in other ways.

}
}

impl Writeable for OurPeerStorage {
Copy link
Collaborator

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?

Copy link
Author

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?

/// 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;
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 think this needs to be a SecretKey? SecretKey implies secp256k1 operations. We really want a [u8; 32], I think.

Copy link
Author

Choose a reason for hiding this comment

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

Done!

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, that was my fault as I had requested it above. No strong opinion though, excuse the noise!

Comment on lines 844 to 845
/// - The derived key must be exactly **32 bytes** and suitable for symmetric
/// encryption algorithms.
Copy link
Collaborator

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.

Copy link
Author

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.

///
/// # Returns
///
/// A [`SecretKey`] representing the encryption key for peer storage.
Copy link
Collaborator

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?

///
/// # Implementation Details
///
/// - The key must be derived from a node-specific secret to ensure uniqueness.
Copy link
Collaborator

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.

Copy link
Author

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.

@adi2011 adi2011 requested review from TheBlueMatt and tnull March 12, 2025 04:22
@adi2011 adi2011 force-pushed the peer-storage/encrypt-decrypt branch from 10d7a4d to 4adeeee Compare March 12, 2025 04:25
Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Feel free to squash!

pub fn create_from_data(key: [u8; 32], ser_channels: Vec<u8>) -> OurPeerStorage {
let n = 0u64;

let mut res = vec![0; ser_channels.len() + 16];
Copy link
Collaborator

Choose a reason for hiding this comment

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

There's no reason to allocate a new vec, just resize ser_channels to add 16 bytes to it and encrypt-in-place into it.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed, Thanks for the suggestions.

const MIN_CYPHERTEXT_LEN: usize = 16;
let cyphertext = &self.encrypted_data[..];

let mut res = vec![0; cyphertext.len() - 16];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here, we can decrypt in-place.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed...

@adi2011 adi2011 force-pushed the peer-storage/encrypt-decrypt branch from 4adeeee to a1f6c9c Compare March 13, 2025 09:33
/// let key = [0u8; 32];
/// let our_peer_storage = OurPeerStorage::create_from_data(key.clone(), vec![1,2,3]);
/// let decrypted_data = our_peer_storage.decrypt_our_peer_storage(key).unwrap();
/// assert_eq!(decrypted_data, vec![1 , 2, 3]);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Stray whitespace after 1.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks

Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't seem fixed?

Copy link
Author

Choose a reason for hiding this comment

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

Really sorry, I did not include this in the fixup.

/// ## Example
/// ```ignore
/// let key = [0u8; 32];
/// let our_peer_storage = OurPeerStorage::create_from_data(key.clone(), vec![1,2,3]);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Add whitespaces after ,s in vec!

Copy link
Author

Choose a reason for hiding this comment

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

Fixed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't seem so?

Self::new(MessageHandler {
chan_handler: channel_message_handler,
route_handler: IgnoringMessageHandler{},
onion_message_handler,
custom_message_handler: IgnoringMessageHandler{},
send_only_message_handler,
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, that is indeed a bit unfortunate. I wonder if it would merit to split out a trait MessageSource holding get_pending_msgs from BaseMessageHandler to at least somewhat reflect this in the interfaces. Also strange that we have a generic message handler there, that in practice would only ever be a concretized type (i.e. ChainMonitor).


let encrypted_data= OurPeerStorage::create_from_data(self.our_peerstorage_encryption_key, Vec::new());
log_debug!(self.logger, "Sending Peer Storage from chainmonitor");
self.pending_send_only_events.lock().unwrap().push(MessageSendEvent::SendPeerStorage { node_id: their_node_id
Copy link
Contributor

Choose a reason for hiding this comment

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

That's because it doesn't operate on the entire codebase yet. We're moving to include more and more files, but you can see the current list of excluded files in rustfmt_excluded_files in the workspace root.

@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, if it's ~a requirement to use the key retrieved via NodeSigner::get_peer_storage_key, why not actually introduce a PeerStorageKey (new)type rather than using a generic [u8; 32] that could be anything?

Aditya Sharma and others added 7 commits March 14, 2025 00:04
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.
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.
This commit replaces magic numbers with descriptive constant names for the indices used in key derivation paths
within the `new` function.

- Added constants:
  - `NODE_SECRET_INDEX`
  - `DESTINATION_SCRIPT_INDEX`
  - `SHUTDOWN_PUBKEY_INDEX`
  - `CHANNEL_MASTER_KEY_INDEX`
  - `INBOUND_PAYMENT_KEY_INDEX`
  - `PEER_STORAGE_KEY_INDEX`
@adi2011 adi2011 force-pushed the peer-storage/encrypt-decrypt branch from a1f6c9c to 00e7e65 Compare March 13, 2025 18:45
@adi2011 adi2011 requested a review from TheBlueMatt March 14, 2025 05:22
Copy link
Contributor

@tnull tnull left a comment

Choose a reason for hiding this comment

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

Some comments but mostly LGTM.

One note: please avoid rebasing on main if not necessary to resolve conflicts and wait with squashing fixups until the reviewers had a chance to look at them. Otherwise it makes it harder to follow which comments from the previous review round have been addressed already. Thanks!

@@ -215,6 +217,9 @@ impl<ChannelSigner: EcdsaChannelSigner> Deref for LockedChannelMonitor<'_, Chann
}
}

/// Represents Secret Key used for encrypting Peer Storage.
type PeerStorageKey = [u8; 32];
Copy link
Contributor

Choose a reason for hiding this comment

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

Having this be a type definition doesn't actually enforce the type really. If we want to do this, let's make it a newtype wrapper, i.e., pub struct PeerStorageKey ([u8; 32]);

Copy link
Author

Choose a reason for hiding this comment

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

Yes, thanks for the suggestion.

/// (serialised channel information), and returns a serialised [`OurPeerStorage`] as a `Vec<u8>`.
///
/// The resulting serialised data is intended to be directly used for transmission to the peers.
pub fn create_from_data(key: [u8; 32], mut ser_channels: Vec<u8>) -> OurPeerStorage {
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to use the new key type here and below, no?

Copy link
Author

Choose a reason for hiding this comment

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

Yes, thanks.

///
/// Thus, if you wish to rely on recovery using this method, you should use a key which
/// can be re-derived from data which would be available after state loss (eg the wallet seed)
fn get_peer_storage_key(&self) -> [u8; 32];
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here, this would need to return PeerStorageKey?

Copy link
Author

Choose a reason for hiding this comment

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

Yup, my bad, Thanks!

@@ -1771,6 +1780,7 @@ pub struct KeysManager {
shutdown_pubkey: PublicKey,
channel_master_key: Xpriv,
channel_child_index: AtomicUsize,
peer_storage_key: SecretKey,
Copy link
Contributor

Choose a reason for hiding this comment

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

If we decided against using SecretKey elsewhere as it isn't going to use ECDSA, this should probably also just store PeerStorageKey or the [u8; 32].

Copy link
Author

Choose a reason for hiding this comment

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

Yes, I have changed the type here to PeerStorageKey.

@adi2011
Copy link
Author

adi2011 commented Mar 14, 2025

@tnull Thank you for the review. I’m really sorry for squashing the fixups, I won’t do that again without approval.

@adi2011 adi2011 requested a review from tnull March 14, 2025 15:39
@adi2011 adi2011 force-pushed the peer-storage/encrypt-decrypt branch from 7082ce2 to c8d181f Compare March 14, 2025 17:38
@dunxen dunxen self-requested a review March 17, 2025 20:15
let plaintext_len = ser_channels.len();

let mut nonce = [0; 12];
nonce[4..].copy_from_slice(&n.to_le_bytes()[..]);

Choose a reason for hiding this comment

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

I noticed that the nonce used for create_from_data and decrypt_our_peer_storage is fixed and always the same. Is this intentional? From what I’ve read, nonces for ChaCha20Poly1305RFC should be unique per encryption for security reasons. The documentation for ChaCha20Poly1305RFC mentions this in the code example. Maybe we can generate a unique nonce (e.g., via RNG or even derive it from the data?)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, if not using a data-derived deterministic nonce, we'll need to take in an EntropySource as a Deref to use its get_secure_random_bytes() method.

Copy link
Contributor

@tnull tnull Mar 18, 2025

Choose a reason for hiding this comment

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

Indeed, reusing the nonce here is likely not a good idea as it would mean losing confidentiality of all messages. I don't think we can just generate a random nonce, as we'd need to store that to be able to use it for decryption. However, the point of peer storage is exactly to be able to recover after having lost to previously-persisted state.

So I wonder if a stream cipher like ChaCha20 is the right choice here to begin with, or if we'd need something block-based in this case.

I guess we could consider putting some random_bytes in the stored message itself and then synthesizing the nonce as SHA256(SHA256(peer_storage_key) + random_bytes)? This would allow us to re-derive the nonce just from the seed and the random_bytes after retrieving the stored message?

Copy link
Author

Choose a reason for hiding this comment

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

Thank you so much for pointing this out @martinsaposnic, I was not aware of the risks of using the same nonce every-time.

@tnull how would we re-derive the same random_bytes while decrypting the blob in this case? Or should we consider replacing ChaCha20 with a different encryption scheme altogether?

Copy link
Contributor

Choose a reason for hiding this comment

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

Excuse the delay here.

@tnull how would we re-derive the same random_bytes while decrypting the blob in this case?

Well, see above for a first idea (although, on second thought, in the implementation a keyed-Hmac might be preferable):

I guess we could consider putting some random_bytes in the stored message itself and then synthesizing the nonce as SHA256(SHA256(peer_storage_key) + random_bytes)?

When we receive a message we previously stored, we could read the random_bytes field from the unencrypted part of the message and re-derive the used nonce as nonce = SHA256(SHA256(peer_storage_key) + random_bytes) (which only we can do as only we know peer_storage_key). We can then use this re-derived synthetic nonce to decrypt the encrypted blob.
Note that a malicious counterparty could manipulate the random_bytes, but that would only result in decryption failures, i.e., data invalidation, which the counterparty can always attain anyways by just not providing the correct blob back to us.

Or should we consider replacing ChaCha20 with a different encryption scheme altogether?

The issue is that, while a block-based encryption scheme that doesn't require a unique nonce might be better suited for this particular usecase, we already have a thoroughly-reviewed implementation of ChaCha20 in our source tree. So if we can, we should make use of it, in particular given that many crypto crates have an unfortunately large dependency tree, etc.

@@ -215,6 +217,23 @@ impl<ChannelSigner: EcdsaChannelSigner> Deref for LockedChannelMonitor<'_, Chann
}
}

/// Represents Secret Key used for encrypting Peer Storage.
#[derive(Clone, PartialEq, Eq)]
pub struct PeerStorageKey ([u8; 32]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's move this to next to the NodeSinger trait in sign/mod.rs rather than have it here.

Copy link
Author

Choose a reason for hiding this comment

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

Agreed, Thanks.

#[derive(Clone, PartialEq, Eq)]
pub struct PeerStorageKey ([u8; 32]);

impl PeerStorageKey {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's drop this impl block and simply make the inner [u8; 32] pub.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed

@@ -735,6 +745,12 @@ where
monitor.block_connected(
header, txdata, height, &*self.broadcaster, &*self.fee_estimator, &self.logger)
});

// Send peer storage everytime a new block arrives.
Copy link
Contributor

Choose a reason for hiding this comment

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

The issue is that due to no-std support we don't have an easy way to access time. So block intervals might be an easy way to get some kind of clock, but we could at least check if we really need to send the new update or not.

/// ## Example
/// ```ignore
/// let key = [0u8; 32];
/// let our_peer_storage = OurPeerStorage::create_from_data(key.clone(), vec![1,2,3]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't seem so?

/// let key = [0u8; 32];
/// let our_peer_storage = OurPeerStorage::create_from_data(key.clone(), vec![1,2,3]);
/// let decrypted_data = our_peer_storage.decrypt_our_peer_storage(key).unwrap();
/// assert_eq!(decrypted_data, vec![1 , 2, 3]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't seem fixed?

let plaintext_len = ser_channels.len();

let mut nonce = [0; 12];
nonce[4..].copy_from_slice(&n.to_le_bytes()[..]);
Copy link
Contributor

@tnull tnull Mar 18, 2025

Choose a reason for hiding this comment

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

Indeed, reusing the nonce here is likely not a good idea as it would mean losing confidentiality of all messages. I don't think we can just generate a random nonce, as we'd need to store that to be able to use it for decryption. However, the point of peer storage is exactly to be able to recover after having lost to previously-persisted state.

So I wonder if a stream cipher like ChaCha20 is the right choice here to begin with, or if we'd need something block-based in this case.

I guess we could consider putting some random_bytes in the stored message itself and then synthesizing the nonce as SHA256(SHA256(peer_storage_key) + random_bytes)? This would allow us to re-derive the nonce just from the seed and the random_bytes after retrieving the stored message?

@adi2011 adi2011 requested a review from tnull March 20, 2025 17:05
Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Feel free to squash the fixups, I believe, which should make this easier to review.

let cyphertext_len = self.encrypted_data.len();

// Split the cyphertext into the encrypted data and the authentication tag.
let (encrypted_data, tag) = self.encrypted_data.split_at_mut(cyphertext_len - 16);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This can panic, I believe.

return Err(());
}

Ok(encrypted_data.to_vec())
Copy link
Collaborator

Choose a reason for hiding this comment

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

We can just resize and return the original vec rather than allocating a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
weekly goal Someone wants to land this this week
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants