-
Notifications
You must be signed in to change notification settings - Fork 390
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
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.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. 🚀 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
?
lightning/src/chain/chainmonitor.rs
Outdated
@@ -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!
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, 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?
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 agree PeerStorageKey
would be better than [u8; 32], what do you think @TheBlueMatt?
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. |
22fe4b2
to
35863ce
Compare
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?
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.
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 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.
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!
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, that was my fault as I had requested it above. No strong opinion though, excuse the noise!
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.
10d7a4d
to
4adeeee
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.
Feel free to squash!
lightning/src/ln/our_peer_storage.rs
Outdated
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]; |
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.
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.
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, Thanks for the suggestions.
lightning/src/ln/our_peer_storage.rs
Outdated
const MIN_CYPHERTEXT_LEN: usize = 16; | ||
let cyphertext = &self.encrypted_data[..]; | ||
|
||
let mut res = vec![0; cyphertext.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.
Same here, we can decrypt in-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.
Fixed...
4adeeee
to
a1f6c9c
Compare
lightning/src/ln/our_peer_storage.rs
Outdated
/// 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]); |
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: Stray whitespace after 1
.
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
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.
Doesn't seem fixed?
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.
Really sorry, I did not include this in the fixup.
lightning/src/ln/our_peer_storage.rs
Outdated
/// ## Example | ||
/// ```ignore | ||
/// let key = [0u8; 32]; | ||
/// let our_peer_storage = OurPeerStorage::create_from_data(key.clone(), vec![1,2,3]); |
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: Add whitespaces after ,
s in vec!
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.
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.
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, |
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.
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
).
lightning/src/chain/chainmonitor.rs
Outdated
|
||
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 |
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'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.
lightning/src/chain/chainmonitor.rs
Outdated
@@ -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.
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?
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`
a1f6c9c
to
00e7e65
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.
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!
lightning/src/chain/chainmonitor.rs
Outdated
@@ -215,6 +217,9 @@ impl<ChannelSigner: EcdsaChannelSigner> Deref for LockedChannelMonitor<'_, Chann | |||
} | |||
} | |||
|
|||
/// Represents Secret Key used for encrypting Peer Storage. | |||
type PeerStorageKey = [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.
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]);
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 the suggestion.
lightning/src/ln/our_peer_storage.rs
Outdated
/// (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 { |
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 need to use the new key
type here and below, 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.
Yes, thanks.
lightning/src/sign/mod.rs
Outdated
/// | ||
/// 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]; |
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.
Same here, this would need to return PeerStorageKey
?
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.
Yup, my bad, Thanks!
lightning/src/sign/mod.rs
Outdated
@@ -1771,6 +1780,7 @@ pub struct KeysManager { | |||
shutdown_pubkey: PublicKey, | |||
channel_master_key: Xpriv, | |||
channel_child_index: AtomicUsize, | |||
peer_storage_key: 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.
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]
.
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 have changed the type here to PeerStorageKey
.
@tnull Thank you for the review. I’m really sorry for squashing the fixups, I won’t do that again without approval. |
7082ce2
to
c8d181f
Compare
let plaintext_len = ser_channels.len(); | ||
|
||
let mut nonce = [0; 12]; | ||
nonce[4..].copy_from_slice(&n.to_le_bytes()[..]); |
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 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?)
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, 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.
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.
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?
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.
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?
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.
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.
lightning/src/chain/chainmonitor.rs
Outdated
@@ -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]); |
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.
Let's move this to next to the NodeSinger
trait in sign/mod.rs
rather than have it 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.
Agreed, Thanks.
lightning/src/chain/chainmonitor.rs
Outdated
#[derive(Clone, PartialEq, Eq)] | ||
pub struct PeerStorageKey ([u8; 32]); | ||
|
||
impl PeerStorageKey { |
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.
Let's drop this impl
block and simply make the inner [u8; 32]
pub
.
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
@@ -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. |
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 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.
lightning/src/ln/our_peer_storage.rs
Outdated
/// ## Example | ||
/// ```ignore | ||
/// let key = [0u8; 32]; | ||
/// let our_peer_storage = OurPeerStorage::create_from_data(key.clone(), vec![1,2,3]); |
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.
Doesn't seem so?
lightning/src/ln/our_peer_storage.rs
Outdated
/// 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]); |
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.
Doesn't seem fixed?
let plaintext_len = ser_channels.len(); | ||
|
||
let mut nonce = [0; 12]; | ||
nonce[4..].copy_from_slice(&n.to_le_bytes()[..]); |
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.
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?
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.
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); |
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 can panic, I believe.
return Err(()); | ||
} | ||
|
||
Ok(encrypted_data.to_vec()) |
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 just resize
and return the original vec rather than allocating a new one.
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.