Add storable_builder helper for client side encryption - #14

Merged
G8XSU merged 2 commits into
lightningdevkit:mainfrom
G8XSU:crypto
Nov 16, 2023
Merged

Add storable_builder helper for client side encryption #14
G8XSU merged 2 commits into
lightningdevkit:mainfrom
G8XSU:crypto

Conversation

@G8XSU

@G8XSUG8XSU commented Nov 2, 2023

Copy link
Copy Markdown
Contributor
  • Add storable_builder helper for client side encryption

  • Note: All of ChaCha20Poly1305 code is copied from rust-lightning repo. (I deleted code which was specifically added for LDK)

  • Note: decrypt_in_place function didn't exist in chacha20poly1305, it is a new addition, reviewers should review it.

@G8XSU
G8XSUforce-pushed the crypto branch 2 times, most recently from b68759d to 8d14179CompareNovember 2, 2023 00:29
@G8XSU
G8XSU requested a review from jkczyzNovember 2, 2023 00:29
@G8XSU
G8XSU marked this pull request as draft November 2, 2023 00:51
@G8XSU
G8XSUforce-pushed the crypto branch 2 times, most recently from 513f7d3 to 3017e9aCompareNovember 2, 2023 01:11
@G8XSU
G8XSU marked this pull request as ready for review November 2, 2023 01:19
@G8XSUG8XSU changed the title Add Storable_Builder helper for client side encryption Add storable_builder helper for client side encryption Nov 2, 2023

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So far just had a look at the first commit. Generally checked that the files generally match (mod reformatting), but have two questions regarding the differences.

self.mac.raw_result(out_tag);
}

pub fn encrypt_inplace(&mut self, input_output: &mut [u8], out_tag: &mut [u8]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new addition seems to be identical to the existing encrypt_full_message_in_place. Can we keep the existing name, so that we eventually know they are the same method when migrating back to rust-lightning?

@G8XSUG8XSUNov 3, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

i found encrypt_in_place more applicable than encrypt_full_message_in_place (i think message here refers to peer-to-peer msgs)
But i am ok with changing it.
This one is not a new addition, only decrypt_inplace is new addition.

Comment threadsrc/crypto/chacha20poly1305.rs
@jkczyz

Copy link
Copy Markdown
  • Note: All of ChaCha20Poly1305 code is copied from rust-lightning repo. (I deleted code which was specifically added for LDK)
  • Note: decrypt_in_place function didn't exist in chacha20poly1305, it is a new addition, reviewers should review it.

Pardon if this has already discussed, but is it worth refactoring code into a new crate to avoid copying? (cc: @TheBlueMatt)

We had a similar issue with some code shared across crates in the rust-lightning repo, but we were able to use symlinking there. Unfortunately, the same can't be done across repos.

@G8XSU

G8XSU commented Nov 3, 2023

Copy link
Copy Markdown
ContributorAuthor

@jkczyz

  1. We can't really depend on rust-lightning repo/crate for chacha20 encryption.

I did discuss it with Matt and Elias earlier,
if we don't want to copy code we can consider adding a dependency on some encryption library.
But it was argued that it is just 1 / 2-file and we can copy it. (and since it should never change, it is ok)

@tnull

tnull commented Nov 6, 2023

Copy link
Copy Markdown
Contributor

FWIW, lightningdevkit/rust-lightning#2708 will add the decrypt_in_place functionality upstream, so yo should be good just copy/pasting the files from upstream once that is merged. This has the benefit that you can just drop them entirely again when the VSS client itself is upstreamed.

@jkczyz

Copy link
Copy Markdown

FWIW, lightningdevkit/rust-lightning#2708 will add the decrypt_in_place functionality upstream, so yo should be good just copy/pasting the files from upstream once that is merged. This has the benefit that you can just drop them entirely again when the VSS client itself is upstreamed.

Oh, I wasn't aware that we were upstreaming this. Do you mean the entire vss-rust-client repo or something else?

@tnull

tnull commented Nov 6, 2023

Copy link
Copy Markdown
Contributor

Oh, I wasn't aware that we were upstreaming this. Do you mean the entire vss-rust-client repo or something else?

I thought there were plans around doing that at least eventually, i.e. have VssStore be part of rust-lightning? Or maybe I'm mistaken?

@G8XSU

G8XSU commented Nov 6, 2023

Copy link
Copy Markdown
ContributorAuthor

Yup, I just saw that we are upstreaming decrypt_in_place. :)

vss-rust-client will not be upstreamed (this PR is vss-rust-client), VssStore will be upstreamed to rust-lightning eventually.

Comment threadCargo.toml Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +27 to +50
Storable {
data: data_blob,
encryption_metadata: Option::from(EncryptionMetadata {
nonce: nonce.to_vec(),
tag: tag.to_vec(),
cipher_format: CHACHA20_CIPHER_NAME.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this result essentially immediately serialized and written to disk? From an allocation perspective, it's a bit unfortunate we have an intermediary object requiring a few Vecs if the alternative is to use something like Writeable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

almost in most cases yes.
main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable.
others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

iiuc, even if it was writeable,
it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

almost in most cases yes. main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable. others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

Any Vec creation other than an empty one results in a heap allocation. So there are two when you use to_vec here and another heap allocation for to_string on CHACHA20_CIPHER_NAME.

iiuc, even if it was writeable, it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

If we didn't use Storable, then we could serialize nonce,tag, and CHACHA20_CIPHER_NAME without creating any new Vecs or Strings . True that it would ultimately put everything into a Vec, so at least one allocation is required.

If we used a rust struct implementing Writeable instead of a proto, we could avoid the unnecessary allocations.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +27 to +50
Storable {
data: data_blob,
encryption_metadata: Option::from(EncryptionMetadata {
nonce: nonce.to_vec(),
tag: tag.to_vec(),
cipher_format: CHACHA20_CIPHER_NAME.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

almost in most cases yes. main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable. others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

Any Vec creation other than an empty one results in a heap allocation. So there are two when you use to_vec here and another heap allocation for to_string on CHACHA20_CIPHER_NAME.

iiuc, even if it was writeable, it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

If we didn't use Storable, then we could serialize nonce,tag, and CHACHA20_CIPHER_NAME without creating any new Vecs or Strings . True that it would ultimately put everything into a Vec, so at least one allocation is required.

If we used a rust struct implementing Writeable instead of a proto, we could avoid the unnecessary allocations.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +40 to +41
nonce: nonce.to_vec(),
tag: tag.to_vec(),
nonce: Vec::from(nonce),
tag: Vec::from(tag),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These still result in heap allocations. A new array is created on the heap in Vec within a Box type and then the data is copied in because arrays implement Copy. Ultimately, the nonce and tag memory is already stack allocated.

Other than foregoing the use of the proto altogether or using an arena (if possible), the only way to avoid excessive heap allocations would be to allocate one Storable as a member of StorableBuilder and reuse its Vec fields by taking slices to use with fill_bytes and encrypt_inplace. Then you would only need to allocate one String for cipher_format, too. But the caller would need to also reuse StorableBuilder, and build couldn't return a Storable any more. Instead, the interface would be in terms of bytes and Storable would be an implementation detail.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Hmm.. yeah, missed the box::new inside of implementation.

Storable as a member of StorableBuilder

Yeah but that introduces a state which mutates per request, making it non-thread safe. I think it will complicate everything.

But what you suggested gave me idea for another approach.
Basically it will avoid Copy and array allocation but not vec allocation. We will create vec's in the first place and use references for fill_bytes and encrypt_inplace, similar to what you suggest.

Since these vec's are fixed size pre-allocated, i think it should be fine?

(Overall i think it shouldn't be a big concern since it is around 28bytes and we allocate much more during just a single gossip msg forward)

(From my limited understanding of glibc allocator, the optimization achieved by using a single Storable as field is easily achieved at allocator level i think. Repeated small allocations will reuse the same small-bins/fast-bins/tcache. It might not avoid alloc traffic but those operations are meant to be fast.
As long as number of small allocations don't keep on growing it is fine.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yeah but that introduces a state which mutates per request, making it non-thread safe. I think it will complicate everything.

You could use a Storable per thread, FWIW.

But what you suggested gave me idea for another approach. Basically it will avoid Copy and array allocation but not vec allocation. We will create vec's in the first place and use references for fill_bytes and encrypt_inplace, similar to what you suggest.

Since these vec's are fixed size pre-allocated, i think it should be fine?

Eh, it removes the stack allocations, which are essentially free, and the copy, yeah.

(Overall i think it shouldn't be a big concern since it is around 28bytes and we allocate much more during just a single gossip msg forward)

The larger concern is not the allocation size but rather any resulting heap fragmentation. I guess your argument is that in comparison to other parts of the code (e.g., gossip) this is trivial or at least similar, IIUC. Most gossip messages are indeed larger but are allocated largely on the stack. Not entirely though as they do contain a Vec field here and there (e.g., features), so I'd buy your argument that perhaps this isn't too much to worry about from a frequency of small allocation perspective.

(From my limited understanding of glibc allocator, the optimization achieved by using a single Storable as field is easily achieved at allocator level i think. Repeated small allocations will reuse the same small-bins/fast-bins/tcache. It might not avoid alloc traffic but those operations are meant to be fast. As long as number of small allocations don't keep on growing it is fine.)

I'm not knowledgable enough here to say what would happen in practice. Essentially, we'll have three small, fixed-size heap allocations for the Vecs and String, which are free'ed whenever the Storeable is freed. Presumably the caller will call encode immediately to pass the value to the KVStore. Afterwards, the Storeable is freed upon drop and thus so are the small allocations.

The question is, will the allocator reuse those small pieces of memory the next time a Storable is created? And will there be much heap fragmentation in the interim?

@TheBlueMatt I'm indifferent on keeping the proto. Seems it's not a whole lot different from other places in the code where there may be comparable allocation patterns (e.g., gossip forwarding). But let me know if I'm missing anything.

@G8XSUG8XSUNov 10, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Most gossip messages are indeed larger but are allocated largely on the stack.

No they are heap allocated, MessageBuf is just an abstraction over Vec link

I did a small instrumentation, for 2000,000 (2million) back to back 16-byte vector allocations, there are only 530 real allocations, 530 bins/slots that get re-used. (Those are 16byte mallocs again and again)

At no point in time did real memory cross those 530 alloc worth space. (so no frag risk i guess, even though there was alloc traffic)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No they are heap allocated, MessageBuf is just an abstraction over Vec link

FWIW, I was referring to the gossip messages after they have been deserialized from a MessageBuf. Those structs' fields are mostly (but not entirely) primitives or other structs that don't require heap allocation.

@G8XSU
G8XSU requested a review from jkczyzNovember 14, 2023 01:57

@jkczyzjkczyz left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Just some minor comments. Feel free to squash fixups and any changes addressing these comments.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
@G8XSU
G8XSU merged commit 3a26735 into lightningdevkit:mainNov 16, 2023
@G8XSUG8XSU mentioned this pull request Nov 28, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@G8XSU@jkczyz@tnull
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add storable_builder helper for client side encryption - #14

Merged
G8XSU merged 2 commits into
lightningdevkit:mainfrom
G8XSU:crypto
Nov 16, 2023
Merged

Add storable_builder helper for client side encryption #14
G8XSU merged 2 commits into
lightningdevkit:mainfrom
G8XSU:crypto

Conversation

@G8XSU

@G8XSUG8XSU commented Nov 2, 2023

Copy link
Copy Markdown
Contributor
  • Add storable_builder helper for client side encryption

  • Note: All of ChaCha20Poly1305 code is copied from rust-lightning repo. (I deleted code which was specifically added for LDK)

  • Note: decrypt_in_place function didn't exist in chacha20poly1305, it is a new addition, reviewers should review it.

@G8XSU
G8XSUforce-pushed the crypto branch 2 times, most recently from b68759d to 8d14179CompareNovember 2, 2023 00:29
@G8XSU
G8XSU requested a review from jkczyzNovember 2, 2023 00:29
@G8XSU
G8XSU marked this pull request as draft November 2, 2023 00:51
@G8XSU
G8XSUforce-pushed the crypto branch 2 times, most recently from 513f7d3 to 3017e9aCompareNovember 2, 2023 01:11
@G8XSU
G8XSU marked this pull request as ready for review November 2, 2023 01:19
@G8XSUG8XSU changed the title Add Storable_Builder helper for client side encryption Add storable_builder helper for client side encryption Nov 2, 2023

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So far just had a look at the first commit. Generally checked that the files generally match (mod reformatting), but have two questions regarding the differences.

self.mac.raw_result(out_tag);
}

pub fn encrypt_inplace(&mut self, input_output: &mut [u8], out_tag: &mut [u8]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new addition seems to be identical to the existing encrypt_full_message_in_place. Can we keep the existing name, so that we eventually know they are the same method when migrating back to rust-lightning?

@G8XSUG8XSUNov 3, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

i found encrypt_in_place more applicable than encrypt_full_message_in_place (i think message here refers to peer-to-peer msgs)
But i am ok with changing it.
This one is not a new addition, only decrypt_inplace is new addition.

Comment threadsrc/crypto/chacha20poly1305.rs
@jkczyz

Copy link
Copy Markdown
  • Note: All of ChaCha20Poly1305 code is copied from rust-lightning repo. (I deleted code which was specifically added for LDK)
  • Note: decrypt_in_place function didn't exist in chacha20poly1305, it is a new addition, reviewers should review it.

Pardon if this has already discussed, but is it worth refactoring code into a new crate to avoid copying? (cc: @TheBlueMatt)

We had a similar issue with some code shared across crates in the rust-lightning repo, but we were able to use symlinking there. Unfortunately, the same can't be done across repos.

@G8XSU

G8XSU commented Nov 3, 2023

Copy link
Copy Markdown
ContributorAuthor

@jkczyz

  1. We can't really depend on rust-lightning repo/crate for chacha20 encryption.

I did discuss it with Matt and Elias earlier,
if we don't want to copy code we can consider adding a dependency on some encryption library.
But it was argued that it is just 1 / 2-file and we can copy it. (and since it should never change, it is ok)

@tnull

tnull commented Nov 6, 2023

Copy link
Copy Markdown
Contributor

FWIW, lightningdevkit/rust-lightning#2708 will add the decrypt_in_place functionality upstream, so yo should be good just copy/pasting the files from upstream once that is merged. This has the benefit that you can just drop them entirely again when the VSS client itself is upstreamed.

@jkczyz

Copy link
Copy Markdown

FWIW, lightningdevkit/rust-lightning#2708 will add the decrypt_in_place functionality upstream, so yo should be good just copy/pasting the files from upstream once that is merged. This has the benefit that you can just drop them entirely again when the VSS client itself is upstreamed.

Oh, I wasn't aware that we were upstreaming this. Do you mean the entire vss-rust-client repo or something else?

@tnull

tnull commented Nov 6, 2023

Copy link
Copy Markdown
Contributor

Oh, I wasn't aware that we were upstreaming this. Do you mean the entire vss-rust-client repo or something else?

I thought there were plans around doing that at least eventually, i.e. have VssStore be part of rust-lightning? Or maybe I'm mistaken?

@G8XSU

G8XSU commented Nov 6, 2023

Copy link
Copy Markdown
ContributorAuthor

Yup, I just saw that we are upstreaming decrypt_in_place. :)

vss-rust-client will not be upstreamed (this PR is vss-rust-client), VssStore will be upstreamed to rust-lightning eventually.

Comment threadCargo.toml Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +27 to +50
Storable {
data: data_blob,
encryption_metadata: Option::from(EncryptionMetadata {
nonce: nonce.to_vec(),
tag: tag.to_vec(),
cipher_format: CHACHA20_CIPHER_NAME.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this result essentially immediately serialized and written to disk? From an allocation perspective, it's a bit unfortunate we have an intermediary object requiring a few Vecs if the alternative is to use something like Writeable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

almost in most cases yes.
main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable.
others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

iiuc, even if it was writeable,
it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

almost in most cases yes. main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable. others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

Any Vec creation other than an empty one results in a heap allocation. So there are two when you use to_vec here and another heap allocation for to_string on CHACHA20_CIPHER_NAME.

iiuc, even if it was writeable, it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

If we didn't use Storable, then we could serialize nonce,tag, and CHACHA20_CIPHER_NAME without creating any new Vecs or Strings . True that it would ultimately put everything into a Vec, so at least one allocation is required.

If we used a rust struct implementing Writeable instead of a proto, we could avoid the unnecessary allocations.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +27 to +50
Storable {
data: data_blob,
encryption_metadata: Option::from(EncryptionMetadata {
nonce: nonce.to_vec(),
tag: tag.to_vec(),
cipher_format: CHACHA20_CIPHER_NAME.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

almost in most cases yes. main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable. others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

Any Vec creation other than an empty one results in a heap allocation. So there are two when you use to_vec here and another heap allocation for to_string on CHACHA20_CIPHER_NAME.

iiuc, even if it was writeable, it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

If we didn't use Storable, then we could serialize nonce,tag, and CHACHA20_CIPHER_NAME without creating any new Vecs or Strings . True that it would ultimately put everything into a Vec, so at least one allocation is required.

If we used a rust struct implementing Writeable instead of a proto, we could avoid the unnecessary allocations.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +40 to +41
nonce: nonce.to_vec(),
tag: tag.to_vec(),
nonce: Vec::from(nonce),
tag: Vec::from(tag),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These still result in heap allocations. A new array is created on the heap in Vec within a Box type and then the data is copied in because arrays implement Copy. Ultimately, the nonce and tag memory is already stack allocated.

Other than foregoing the use of the proto altogether or using an arena (if possible), the only way to avoid excessive heap allocations would be to allocate one Storable as a member of StorableBuilder and reuse its Vec fields by taking slices to use with fill_bytes and encrypt_inplace. Then you would only need to allocate one String for cipher_format, too. But the caller would need to also reuse StorableBuilder, and build couldn't return a Storable any more. Instead, the interface would be in terms of bytes and Storable would be an implementation detail.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Hmm.. yeah, missed the box::new inside of implementation.

Storable as a member of StorableBuilder

Yeah but that introduces a state which mutates per request, making it non-thread safe. I think it will complicate everything.

But what you suggested gave me idea for another approach.
Basically it will avoid Copy and array allocation but not vec allocation. We will create vec's in the first place and use references for fill_bytes and encrypt_inplace, similar to what you suggest.

Since these vec's are fixed size pre-allocated, i think it should be fine?

(Overall i think it shouldn't be a big concern since it is around 28bytes and we allocate much more during just a single gossip msg forward)

(From my limited understanding of glibc allocator, the optimization achieved by using a single Storable as field is easily achieved at allocator level i think. Repeated small allocations will reuse the same small-bins/fast-bins/tcache. It might not avoid alloc traffic but those operations are meant to be fast.
As long as number of small allocations don't keep on growing it is fine.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yeah but that introduces a state which mutates per request, making it non-thread safe. I think it will complicate everything.

You could use a Storable per thread, FWIW.

But what you suggested gave me idea for another approach. Basically it will avoid Copy and array allocation but not vec allocation. We will create vec's in the first place and use references for fill_bytes and encrypt_inplace, similar to what you suggest.

Since these vec's are fixed size pre-allocated, i think it should be fine?

Eh, it removes the stack allocations, which are essentially free, and the copy, yeah.

(Overall i think it shouldn't be a big concern since it is around 28bytes and we allocate much more during just a single gossip msg forward)

The larger concern is not the allocation size but rather any resulting heap fragmentation. I guess your argument is that in comparison to other parts of the code (e.g., gossip) this is trivial or at least similar, IIUC. Most gossip messages are indeed larger but are allocated largely on the stack. Not entirely though as they do contain a Vec field here and there (e.g., features), so I'd buy your argument that perhaps this isn't too much to worry about from a frequency of small allocation perspective.

(From my limited understanding of glibc allocator, the optimization achieved by using a single Storable as field is easily achieved at allocator level i think. Repeated small allocations will reuse the same small-bins/fast-bins/tcache. It might not avoid alloc traffic but those operations are meant to be fast. As long as number of small allocations don't keep on growing it is fine.)

I'm not knowledgable enough here to say what would happen in practice. Essentially, we'll have three small, fixed-size heap allocations for the Vecs and String, which are free'ed whenever the Storeable is freed. Presumably the caller will call encode immediately to pass the value to the KVStore. Afterwards, the Storeable is freed upon drop and thus so are the small allocations.

The question is, will the allocator reuse those small pieces of memory the next time a Storable is created? And will there be much heap fragmentation in the interim?

@TheBlueMatt I'm indifferent on keeping the proto. Seems it's not a whole lot different from other places in the code where there may be comparable allocation patterns (e.g., gossip forwarding). But let me know if I'm missing anything.

@G8XSUG8XSUNov 10, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Most gossip messages are indeed larger but are allocated largely on the stack.

No they are heap allocated, MessageBuf is just an abstraction over Vec link

I did a small instrumentation, for 2000,000 (2million) back to back 16-byte vector allocations, there are only 530 real allocations, 530 bins/slots that get re-used. (Those are 16byte mallocs again and again)

At no point in time did real memory cross those 530 alloc worth space. (so no frag risk i guess, even though there was alloc traffic)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No they are heap allocated, MessageBuf is just an abstraction over Vec link

FWIW, I was referring to the gossip messages after they have been deserialized from a MessageBuf. Those structs' fields are mostly (but not entirely) primitives or other structs that don't require heap allocation.

@G8XSU
G8XSU requested a review from jkczyzNovember 14, 2023 01:57

@jkczyzjkczyz left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Just some minor comments. Feel free to squash fixups and any changes addressing these comments.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
@G8XSU
G8XSU merged commit 3a26735 into lightningdevkit:mainNov 16, 2023
@G8XSUG8XSU mentioned this pull request Nov 28, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@G8XSU@jkczyz@tnull
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add storable_builder helper for client side encryption - #14

Merged
G8XSU merged 2 commits into
lightningdevkit:mainfrom
G8XSU:crypto
Nov 16, 2023
Merged

Add storable_builder helper for client side encryption #14
G8XSU merged 2 commits into
lightningdevkit:mainfrom
G8XSU:crypto

Conversation

@G8XSU

@G8XSUG8XSU commented Nov 2, 2023

Copy link
Copy Markdown
Contributor
  • Add storable_builder helper for client side encryption

  • Note: All of ChaCha20Poly1305 code is copied from rust-lightning repo. (I deleted code which was specifically added for LDK)

  • Note: decrypt_in_place function didn't exist in chacha20poly1305, it is a new addition, reviewers should review it.

@G8XSU
G8XSUforce-pushed the crypto branch 2 times, most recently from b68759d to 8d14179CompareNovember 2, 2023 00:29
@G8XSU
G8XSU requested a review from jkczyzNovember 2, 2023 00:29
@G8XSU
G8XSU marked this pull request as draft November 2, 2023 00:51
@G8XSU
G8XSUforce-pushed the crypto branch 2 times, most recently from 513f7d3 to 3017e9aCompareNovember 2, 2023 01:11
@G8XSU
G8XSU marked this pull request as ready for review November 2, 2023 01:19
@G8XSUG8XSU changed the title Add Storable_Builder helper for client side encryption Add storable_builder helper for client side encryption Nov 2, 2023

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So far just had a look at the first commit. Generally checked that the files generally match (mod reformatting), but have two questions regarding the differences.

self.mac.raw_result(out_tag);
}

pub fn encrypt_inplace(&mut self, input_output: &mut [u8], out_tag: &mut [u8]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new addition seems to be identical to the existing encrypt_full_message_in_place. Can we keep the existing name, so that we eventually know they are the same method when migrating back to rust-lightning?

@G8XSUG8XSUNov 3, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

i found encrypt_in_place more applicable than encrypt_full_message_in_place (i think message here refers to peer-to-peer msgs)
But i am ok with changing it.
This one is not a new addition, only decrypt_inplace is new addition.

Comment threadsrc/crypto/chacha20poly1305.rs
@jkczyz

Copy link
Copy Markdown
  • Note: All of ChaCha20Poly1305 code is copied from rust-lightning repo. (I deleted code which was specifically added for LDK)
  • Note: decrypt_in_place function didn't exist in chacha20poly1305, it is a new addition, reviewers should review it.

Pardon if this has already discussed, but is it worth refactoring code into a new crate to avoid copying? (cc: @TheBlueMatt)

We had a similar issue with some code shared across crates in the rust-lightning repo, but we were able to use symlinking there. Unfortunately, the same can't be done across repos.

@G8XSU

G8XSU commented Nov 3, 2023

Copy link
Copy Markdown
ContributorAuthor

@jkczyz

  1. We can't really depend on rust-lightning repo/crate for chacha20 encryption.

I did discuss it with Matt and Elias earlier,
if we don't want to copy code we can consider adding a dependency on some encryption library.
But it was argued that it is just 1 / 2-file and we can copy it. (and since it should never change, it is ok)

@tnull

tnull commented Nov 6, 2023

Copy link
Copy Markdown
Contributor

FWIW, lightningdevkit/rust-lightning#2708 will add the decrypt_in_place functionality upstream, so yo should be good just copy/pasting the files from upstream once that is merged. This has the benefit that you can just drop them entirely again when the VSS client itself is upstreamed.

@jkczyz

Copy link
Copy Markdown

FWIW, lightningdevkit/rust-lightning#2708 will add the decrypt_in_place functionality upstream, so yo should be good just copy/pasting the files from upstream once that is merged. This has the benefit that you can just drop them entirely again when the VSS client itself is upstreamed.

Oh, I wasn't aware that we were upstreaming this. Do you mean the entire vss-rust-client repo or something else?

@tnull

tnull commented Nov 6, 2023

Copy link
Copy Markdown
Contributor

Oh, I wasn't aware that we were upstreaming this. Do you mean the entire vss-rust-client repo or something else?

I thought there were plans around doing that at least eventually, i.e. have VssStore be part of rust-lightning? Or maybe I'm mistaken?

@G8XSU

G8XSU commented Nov 6, 2023

Copy link
Copy Markdown
ContributorAuthor

Yup, I just saw that we are upstreaming decrypt_in_place. :)

vss-rust-client will not be upstreamed (this PR is vss-rust-client), VssStore will be upstreamed to rust-lightning eventually.

Comment threadCargo.toml Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +27 to +50
Storable {
data: data_blob,
encryption_metadata: Option::from(EncryptionMetadata {
nonce: nonce.to_vec(),
tag: tag.to_vec(),
cipher_format: CHACHA20_CIPHER_NAME.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this result essentially immediately serialized and written to disk? From an allocation perspective, it's a bit unfortunate we have an intermediary object requiring a few Vecs if the alternative is to use something like Writeable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

almost in most cases yes.
main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable.
others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

iiuc, even if it was writeable,
it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

almost in most cases yes. main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable. others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

Any Vec creation other than an empty one results in a heap allocation. So there are two when you use to_vec here and another heap allocation for to_string on CHACHA20_CIPHER_NAME.

iiuc, even if it was writeable, it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

If we didn't use Storable, then we could serialize nonce,tag, and CHACHA20_CIPHER_NAME without creating any new Vecs or Strings . True that it would ultimately put everything into a Vec, so at least one allocation is required.

If we used a rust struct implementing Writeable instead of a proto, we could avoid the unnecessary allocations.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +27 to +50
Storable {
data: data_blob,
encryption_metadata: Option::from(EncryptionMetadata {
nonce: nonce.to_vec(),
tag: tag.to_vec(),
cipher_format: CHACHA20_CIPHER_NAME.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

almost in most cases yes. main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable. others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

Any Vec creation other than an empty one results in a heap allocation. So there are two when you use to_vec here and another heap allocation for to_string on CHACHA20_CIPHER_NAME.

iiuc, even if it was writeable, it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

If we didn't use Storable, then we could serialize nonce,tag, and CHACHA20_CIPHER_NAME without creating any new Vecs or Strings . True that it would ultimately put everything into a Vec, so at least one allocation is required.

If we used a rust struct implementing Writeable instead of a proto, we could avoid the unnecessary allocations.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +40 to +41
nonce: nonce.to_vec(),
tag: tag.to_vec(),
nonce: Vec::from(nonce),
tag: Vec::from(tag),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These still result in heap allocations. A new array is created on the heap in Vec within a Box type and then the data is copied in because arrays implement Copy. Ultimately, the nonce and tag memory is already stack allocated.

Other than foregoing the use of the proto altogether or using an arena (if possible), the only way to avoid excessive heap allocations would be to allocate one Storable as a member of StorableBuilder and reuse its Vec fields by taking slices to use with fill_bytes and encrypt_inplace. Then you would only need to allocate one String for cipher_format, too. But the caller would need to also reuse StorableBuilder, and build couldn't return a Storable any more. Instead, the interface would be in terms of bytes and Storable would be an implementation detail.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Hmm.. yeah, missed the box::new inside of implementation.

Storable as a member of StorableBuilder

Yeah but that introduces a state which mutates per request, making it non-thread safe. I think it will complicate everything.

But what you suggested gave me idea for another approach.
Basically it will avoid Copy and array allocation but not vec allocation. We will create vec's in the first place and use references for fill_bytes and encrypt_inplace, similar to what you suggest.

Since these vec's are fixed size pre-allocated, i think it should be fine?

(Overall i think it shouldn't be a big concern since it is around 28bytes and we allocate much more during just a single gossip msg forward)

(From my limited understanding of glibc allocator, the optimization achieved by using a single Storable as field is easily achieved at allocator level i think. Repeated small allocations will reuse the same small-bins/fast-bins/tcache. It might not avoid alloc traffic but those operations are meant to be fast.
As long as number of small allocations don't keep on growing it is fine.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yeah but that introduces a state which mutates per request, making it non-thread safe. I think it will complicate everything.

You could use a Storable per thread, FWIW.

But what you suggested gave me idea for another approach. Basically it will avoid Copy and array allocation but not vec allocation. We will create vec's in the first place and use references for fill_bytes and encrypt_inplace, similar to what you suggest.

Since these vec's are fixed size pre-allocated, i think it should be fine?

Eh, it removes the stack allocations, which are essentially free, and the copy, yeah.

(Overall i think it shouldn't be a big concern since it is around 28bytes and we allocate much more during just a single gossip msg forward)

The larger concern is not the allocation size but rather any resulting heap fragmentation. I guess your argument is that in comparison to other parts of the code (e.g., gossip) this is trivial or at least similar, IIUC. Most gossip messages are indeed larger but are allocated largely on the stack. Not entirely though as they do contain a Vec field here and there (e.g., features), so I'd buy your argument that perhaps this isn't too much to worry about from a frequency of small allocation perspective.

(From my limited understanding of glibc allocator, the optimization achieved by using a single Storable as field is easily achieved at allocator level i think. Repeated small allocations will reuse the same small-bins/fast-bins/tcache. It might not avoid alloc traffic but those operations are meant to be fast. As long as number of small allocations don't keep on growing it is fine.)

I'm not knowledgable enough here to say what would happen in practice. Essentially, we'll have three small, fixed-size heap allocations for the Vecs and String, which are free'ed whenever the Storeable is freed. Presumably the caller will call encode immediately to pass the value to the KVStore. Afterwards, the Storeable is freed upon drop and thus so are the small allocations.

The question is, will the allocator reuse those small pieces of memory the next time a Storable is created? And will there be much heap fragmentation in the interim?

@TheBlueMatt I'm indifferent on keeping the proto. Seems it's not a whole lot different from other places in the code where there may be comparable allocation patterns (e.g., gossip forwarding). But let me know if I'm missing anything.

@G8XSUG8XSUNov 10, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Most gossip messages are indeed larger but are allocated largely on the stack.

No they are heap allocated, MessageBuf is just an abstraction over Vec link

I did a small instrumentation, for 2000,000 (2million) back to back 16-byte vector allocations, there are only 530 real allocations, 530 bins/slots that get re-used. (Those are 16byte mallocs again and again)

At no point in time did real memory cross those 530 alloc worth space. (so no frag risk i guess, even though there was alloc traffic)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No they are heap allocated, MessageBuf is just an abstraction over Vec link

FWIW, I was referring to the gossip messages after they have been deserialized from a MessageBuf. Those structs' fields are mostly (but not entirely) primitives or other structs that don't require heap allocation.

@G8XSU
G8XSU requested a review from jkczyzNovember 14, 2023 01:57

@jkczyzjkczyz left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Just some minor comments. Feel free to squash fixups and any changes addressing these comments.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
@G8XSU
G8XSU merged commit 3a26735 into lightningdevkit:mainNov 16, 2023
@G8XSUG8XSU mentioned this pull request Nov 28, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@G8XSU@jkczyz@tnull
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add storable_builder helper for client side encryption - #14

Merged
G8XSU merged 2 commits into
lightningdevkit:mainfrom
G8XSU:crypto
Nov 16, 2023
Merged

Add storable_builder helper for client side encryption #14
G8XSU merged 2 commits into
lightningdevkit:mainfrom
G8XSU:crypto

Conversation

@G8XSU

@G8XSUG8XSU commented Nov 2, 2023

Copy link
Copy Markdown
Contributor
  • Add storable_builder helper for client side encryption

  • Note: All of ChaCha20Poly1305 code is copied from rust-lightning repo. (I deleted code which was specifically added for LDK)

  • Note: decrypt_in_place function didn't exist in chacha20poly1305, it is a new addition, reviewers should review it.

@G8XSU
G8XSUforce-pushed the crypto branch 2 times, most recently from b68759d to 8d14179CompareNovember 2, 2023 00:29
@G8XSU
G8XSU requested a review from jkczyzNovember 2, 2023 00:29
@G8XSU
G8XSU marked this pull request as draft November 2, 2023 00:51
@G8XSU
G8XSUforce-pushed the crypto branch 2 times, most recently from 513f7d3 to 3017e9aCompareNovember 2, 2023 01:11
@G8XSU
G8XSU marked this pull request as ready for review November 2, 2023 01:19
@G8XSUG8XSU changed the title Add Storable_Builder helper for client side encryption Add storable_builder helper for client side encryption Nov 2, 2023

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So far just had a look at the first commit. Generally checked that the files generally match (mod reformatting), but have two questions regarding the differences.

self.mac.raw_result(out_tag);
}

pub fn encrypt_inplace(&mut self, input_output: &mut [u8], out_tag: &mut [u8]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new addition seems to be identical to the existing encrypt_full_message_in_place. Can we keep the existing name, so that we eventually know they are the same method when migrating back to rust-lightning?

@G8XSUG8XSUNov 3, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

i found encrypt_in_place more applicable than encrypt_full_message_in_place (i think message here refers to peer-to-peer msgs)
But i am ok with changing it.
This one is not a new addition, only decrypt_inplace is new addition.

Comment threadsrc/crypto/chacha20poly1305.rs
@jkczyz

Copy link
Copy Markdown
  • Note: All of ChaCha20Poly1305 code is copied from rust-lightning repo. (I deleted code which was specifically added for LDK)
  • Note: decrypt_in_place function didn't exist in chacha20poly1305, it is a new addition, reviewers should review it.

Pardon if this has already discussed, but is it worth refactoring code into a new crate to avoid copying? (cc: @TheBlueMatt)

We had a similar issue with some code shared across crates in the rust-lightning repo, but we were able to use symlinking there. Unfortunately, the same can't be done across repos.

@G8XSU

G8XSU commented Nov 3, 2023

Copy link
Copy Markdown
ContributorAuthor

@jkczyz

  1. We can't really depend on rust-lightning repo/crate for chacha20 encryption.

I did discuss it with Matt and Elias earlier,
if we don't want to copy code we can consider adding a dependency on some encryption library.
But it was argued that it is just 1 / 2-file and we can copy it. (and since it should never change, it is ok)

@tnull

tnull commented Nov 6, 2023

Copy link
Copy Markdown
Contributor

FWIW, lightningdevkit/rust-lightning#2708 will add the decrypt_in_place functionality upstream, so yo should be good just copy/pasting the files from upstream once that is merged. This has the benefit that you can just drop them entirely again when the VSS client itself is upstreamed.

@jkczyz

Copy link
Copy Markdown

FWIW, lightningdevkit/rust-lightning#2708 will add the decrypt_in_place functionality upstream, so yo should be good just copy/pasting the files from upstream once that is merged. This has the benefit that you can just drop them entirely again when the VSS client itself is upstreamed.

Oh, I wasn't aware that we were upstreaming this. Do you mean the entire vss-rust-client repo or something else?

@tnull

tnull commented Nov 6, 2023

Copy link
Copy Markdown
Contributor

Oh, I wasn't aware that we were upstreaming this. Do you mean the entire vss-rust-client repo or something else?

I thought there were plans around doing that at least eventually, i.e. have VssStore be part of rust-lightning? Or maybe I'm mistaken?

@G8XSU

G8XSU commented Nov 6, 2023

Copy link
Copy Markdown
ContributorAuthor

Yup, I just saw that we are upstreaming decrypt_in_place. :)

vss-rust-client will not be upstreamed (this PR is vss-rust-client), VssStore will be upstreamed to rust-lightning eventually.

Comment threadCargo.toml Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +27 to +50
Storable {
data: data_blob,
encryption_metadata: Option::from(EncryptionMetadata {
nonce: nonce.to_vec(),
tag: tag.to_vec(),
cipher_format: CHACHA20_CIPHER_NAME.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this result essentially immediately serialized and written to disk? From an allocation perspective, it's a bit unfortunate we have an intermediary object requiring a few Vecs if the alternative is to use something like Writeable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

almost in most cases yes.
main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable.
others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

iiuc, even if it was writeable,
it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

almost in most cases yes. main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable. others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

Any Vec creation other than an empty one results in a heap allocation. So there are two when you use to_vec here and another heap allocation for to_string on CHACHA20_CIPHER_NAME.

iiuc, even if it was writeable, it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

If we didn't use Storable, then we could serialize nonce,tag, and CHACHA20_CIPHER_NAME without creating any new Vecs or Strings . True that it would ultimately put everything into a Vec, so at least one allocation is required.

If we used a rust struct implementing Writeable instead of a proto, we could avoid the unnecessary allocations.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +27 to +50
Storable {
data: data_blob,
encryption_metadata: Option::from(EncryptionMetadata {
nonce: nonce.to_vec(),
tag: tag.to_vec(),
cipher_format: CHACHA20_CIPHER_NAME.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

almost in most cases yes. main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable. others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

Any Vec creation other than an empty one results in a heap allocation. So there are two when you use to_vec here and another heap allocation for to_string on CHACHA20_CIPHER_NAME.

iiuc, even if it was writeable, it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

If we didn't use Storable, then we could serialize nonce,tag, and CHACHA20_CIPHER_NAME without creating any new Vecs or Strings . True that it would ultimately put everything into a Vec, so at least one allocation is required.

If we used a rust struct implementing Writeable instead of a proto, we could avoid the unnecessary allocations.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +40 to +41
nonce: nonce.to_vec(),
tag: tag.to_vec(),
nonce: Vec::from(nonce),
tag: Vec::from(tag),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These still result in heap allocations. A new array is created on the heap in Vec within a Box type and then the data is copied in because arrays implement Copy. Ultimately, the nonce and tag memory is already stack allocated.

Other than foregoing the use of the proto altogether or using an arena (if possible), the only way to avoid excessive heap allocations would be to allocate one Storable as a member of StorableBuilder and reuse its Vec fields by taking slices to use with fill_bytes and encrypt_inplace. Then you would only need to allocate one String for cipher_format, too. But the caller would need to also reuse StorableBuilder, and build couldn't return a Storable any more. Instead, the interface would be in terms of bytes and Storable would be an implementation detail.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Hmm.. yeah, missed the box::new inside of implementation.

Storable as a member of StorableBuilder

Yeah but that introduces a state which mutates per request, making it non-thread safe. I think it will complicate everything.

But what you suggested gave me idea for another approach.
Basically it will avoid Copy and array allocation but not vec allocation. We will create vec's in the first place and use references for fill_bytes and encrypt_inplace, similar to what you suggest.

Since these vec's are fixed size pre-allocated, i think it should be fine?

(Overall i think it shouldn't be a big concern since it is around 28bytes and we allocate much more during just a single gossip msg forward)

(From my limited understanding of glibc allocator, the optimization achieved by using a single Storable as field is easily achieved at allocator level i think. Repeated small allocations will reuse the same small-bins/fast-bins/tcache. It might not avoid alloc traffic but those operations are meant to be fast.
As long as number of small allocations don't keep on growing it is fine.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yeah but that introduces a state which mutates per request, making it non-thread safe. I think it will complicate everything.

You could use a Storable per thread, FWIW.

But what you suggested gave me idea for another approach. Basically it will avoid Copy and array allocation but not vec allocation. We will create vec's in the first place and use references for fill_bytes and encrypt_inplace, similar to what you suggest.

Since these vec's are fixed size pre-allocated, i think it should be fine?

Eh, it removes the stack allocations, which are essentially free, and the copy, yeah.

(Overall i think it shouldn't be a big concern since it is around 28bytes and we allocate much more during just a single gossip msg forward)

The larger concern is not the allocation size but rather any resulting heap fragmentation. I guess your argument is that in comparison to other parts of the code (e.g., gossip) this is trivial or at least similar, IIUC. Most gossip messages are indeed larger but are allocated largely on the stack. Not entirely though as they do contain a Vec field here and there (e.g., features), so I'd buy your argument that perhaps this isn't too much to worry about from a frequency of small allocation perspective.

(From my limited understanding of glibc allocator, the optimization achieved by using a single Storable as field is easily achieved at allocator level i think. Repeated small allocations will reuse the same small-bins/fast-bins/tcache. It might not avoid alloc traffic but those operations are meant to be fast. As long as number of small allocations don't keep on growing it is fine.)

I'm not knowledgable enough here to say what would happen in practice. Essentially, we'll have three small, fixed-size heap allocations for the Vecs and String, which are free'ed whenever the Storeable is freed. Presumably the caller will call encode immediately to pass the value to the KVStore. Afterwards, the Storeable is freed upon drop and thus so are the small allocations.

The question is, will the allocator reuse those small pieces of memory the next time a Storable is created? And will there be much heap fragmentation in the interim?

@TheBlueMatt I'm indifferent on keeping the proto. Seems it's not a whole lot different from other places in the code where there may be comparable allocation patterns (e.g., gossip forwarding). But let me know if I'm missing anything.

@G8XSUG8XSUNov 10, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Most gossip messages are indeed larger but are allocated largely on the stack.

No they are heap allocated, MessageBuf is just an abstraction over Vec link

I did a small instrumentation, for 2000,000 (2million) back to back 16-byte vector allocations, there are only 530 real allocations, 530 bins/slots that get re-used. (Those are 16byte mallocs again and again)

At no point in time did real memory cross those 530 alloc worth space. (so no frag risk i guess, even though there was alloc traffic)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No they are heap allocated, MessageBuf is just an abstraction over Vec link

FWIW, I was referring to the gossip messages after they have been deserialized from a MessageBuf. Those structs' fields are mostly (but not entirely) primitives or other structs that don't require heap allocation.

@G8XSU
G8XSU requested a review from jkczyzNovember 14, 2023 01:57

@jkczyzjkczyz left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Just some minor comments. Feel free to squash fixups and any changes addressing these comments.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
@G8XSU
G8XSU merged commit 3a26735 into lightningdevkit:mainNov 16, 2023
@G8XSUG8XSU mentioned this pull request Nov 28, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@G8XSU@jkczyz@tnull
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add storable_builder helper for client side encryption - #14

Merged
G8XSU merged 2 commits into
lightningdevkit:mainfrom
G8XSU:crypto
Nov 16, 2023
Merged

Add storable_builder helper for client side encryption #14
G8XSU merged 2 commits into
lightningdevkit:mainfrom
G8XSU:crypto

Conversation

@G8XSU

@G8XSUG8XSU commented Nov 2, 2023

Copy link
Copy Markdown
Contributor
  • Add storable_builder helper for client side encryption

  • Note: All of ChaCha20Poly1305 code is copied from rust-lightning repo. (I deleted code which was specifically added for LDK)

  • Note: decrypt_in_place function didn't exist in chacha20poly1305, it is a new addition, reviewers should review it.

@G8XSU
G8XSUforce-pushed the crypto branch 2 times, most recently from b68759d to 8d14179CompareNovember 2, 2023 00:29
@G8XSU
G8XSU requested a review from jkczyzNovember 2, 2023 00:29
@G8XSU
G8XSU marked this pull request as draft November 2, 2023 00:51
@G8XSU
G8XSUforce-pushed the crypto branch 2 times, most recently from 513f7d3 to 3017e9aCompareNovember 2, 2023 01:11
@G8XSU
G8XSU marked this pull request as ready for review November 2, 2023 01:19
@G8XSUG8XSU changed the title Add Storable_Builder helper for client side encryption Add storable_builder helper for client side encryption Nov 2, 2023

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So far just had a look at the first commit. Generally checked that the files generally match (mod reformatting), but have two questions regarding the differences.

self.mac.raw_result(out_tag);
}

pub fn encrypt_inplace(&mut self, input_output: &mut [u8], out_tag: &mut [u8]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new addition seems to be identical to the existing encrypt_full_message_in_place. Can we keep the existing name, so that we eventually know they are the same method when migrating back to rust-lightning?

@G8XSUG8XSUNov 3, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

i found encrypt_in_place more applicable than encrypt_full_message_in_place (i think message here refers to peer-to-peer msgs)
But i am ok with changing it.
This one is not a new addition, only decrypt_inplace is new addition.

Comment threadsrc/crypto/chacha20poly1305.rs
@jkczyz

Copy link
Copy Markdown
  • Note: All of ChaCha20Poly1305 code is copied from rust-lightning repo. (I deleted code which was specifically added for LDK)
  • Note: decrypt_in_place function didn't exist in chacha20poly1305, it is a new addition, reviewers should review it.

Pardon if this has already discussed, but is it worth refactoring code into a new crate to avoid copying? (cc: @TheBlueMatt)

We had a similar issue with some code shared across crates in the rust-lightning repo, but we were able to use symlinking there. Unfortunately, the same can't be done across repos.

@G8XSU

G8XSU commented Nov 3, 2023

Copy link
Copy Markdown
ContributorAuthor

@jkczyz

  1. We can't really depend on rust-lightning repo/crate for chacha20 encryption.

I did discuss it with Matt and Elias earlier,
if we don't want to copy code we can consider adding a dependency on some encryption library.
But it was argued that it is just 1 / 2-file and we can copy it. (and since it should never change, it is ok)

@tnull

tnull commented Nov 6, 2023

Copy link
Copy Markdown
Contributor

FWIW, lightningdevkit/rust-lightning#2708 will add the decrypt_in_place functionality upstream, so yo should be good just copy/pasting the files from upstream once that is merged. This has the benefit that you can just drop them entirely again when the VSS client itself is upstreamed.

@jkczyz

Copy link
Copy Markdown

FWIW, lightningdevkit/rust-lightning#2708 will add the decrypt_in_place functionality upstream, so yo should be good just copy/pasting the files from upstream once that is merged. This has the benefit that you can just drop them entirely again when the VSS client itself is upstreamed.

Oh, I wasn't aware that we were upstreaming this. Do you mean the entire vss-rust-client repo or something else?

@tnull

tnull commented Nov 6, 2023

Copy link
Copy Markdown
Contributor

Oh, I wasn't aware that we were upstreaming this. Do you mean the entire vss-rust-client repo or something else?

I thought there were plans around doing that at least eventually, i.e. have VssStore be part of rust-lightning? Or maybe I'm mistaken?

@G8XSU

G8XSU commented Nov 6, 2023

Copy link
Copy Markdown
ContributorAuthor

Yup, I just saw that we are upstreaming decrypt_in_place. :)

vss-rust-client will not be upstreamed (this PR is vss-rust-client), VssStore will be upstreamed to rust-lightning eventually.

Comment threadCargo.toml Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +27 to +50
Storable {
data: data_blob,
encryption_metadata: Option::from(EncryptionMetadata {
nonce: nonce.to_vec(),
tag: tag.to_vec(),
cipher_format: CHACHA20_CIPHER_NAME.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this result essentially immediately serialized and written to disk? From an allocation perspective, it's a bit unfortunate we have an intermediary object requiring a few Vecs if the alternative is to use something like Writeable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

almost in most cases yes.
main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable.
others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

iiuc, even if it was writeable,
it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

almost in most cases yes. main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable. others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

Any Vec creation other than an empty one results in a heap allocation. So there are two when you use to_vec here and another heap allocation for to_string on CHACHA20_CIPHER_NAME.

iiuc, even if it was writeable, it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

If we didn't use Storable, then we could serialize nonce,tag, and CHACHA20_CIPHER_NAME without creating any new Vecs or Strings . True that it would ultimately put everything into a Vec, so at least one allocation is required.

If we used a rust struct implementing Writeable instead of a proto, we could avoid the unnecessary allocations.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +27 to +50
Storable {
data: data_blob,
encryption_metadata: Option::from(EncryptionMetadata {
nonce: nonce.to_vec(),
tag: tag.to_vec(),
cipher_format: CHACHA20_CIPHER_NAME.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

almost in most cases yes. main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable. others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

Any Vec creation other than an empty one results in a heap allocation. So there are two when you use to_vec here and another heap allocation for to_string on CHACHA20_CIPHER_NAME.

iiuc, even if it was writeable, it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

If we didn't use Storable, then we could serialize nonce,tag, and CHACHA20_CIPHER_NAME without creating any new Vecs or Strings . True that it would ultimately put everything into a Vec, so at least one allocation is required.

If we used a rust struct implementing Writeable instead of a proto, we could avoid the unnecessary allocations.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +40 to +41
nonce: nonce.to_vec(),
tag: tag.to_vec(),
nonce: Vec::from(nonce),
tag: Vec::from(tag),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These still result in heap allocations. A new array is created on the heap in Vec within a Box type and then the data is copied in because arrays implement Copy. Ultimately, the nonce and tag memory is already stack allocated.

Other than foregoing the use of the proto altogether or using an arena (if possible), the only way to avoid excessive heap allocations would be to allocate one Storable as a member of StorableBuilder and reuse its Vec fields by taking slices to use with fill_bytes and encrypt_inplace. Then you would only need to allocate one String for cipher_format, too. But the caller would need to also reuse StorableBuilder, and build couldn't return a Storable any more. Instead, the interface would be in terms of bytes and Storable would be an implementation detail.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Hmm.. yeah, missed the box::new inside of implementation.

Storable as a member of StorableBuilder

Yeah but that introduces a state which mutates per request, making it non-thread safe. I think it will complicate everything.

But what you suggested gave me idea for another approach.
Basically it will avoid Copy and array allocation but not vec allocation. We will create vec's in the first place and use references for fill_bytes and encrypt_inplace, similar to what you suggest.

Since these vec's are fixed size pre-allocated, i think it should be fine?

(Overall i think it shouldn't be a big concern since it is around 28bytes and we allocate much more during just a single gossip msg forward)

(From my limited understanding of glibc allocator, the optimization achieved by using a single Storable as field is easily achieved at allocator level i think. Repeated small allocations will reuse the same small-bins/fast-bins/tcache. It might not avoid alloc traffic but those operations are meant to be fast.
As long as number of small allocations don't keep on growing it is fine.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yeah but that introduces a state which mutates per request, making it non-thread safe. I think it will complicate everything.

You could use a Storable per thread, FWIW.

But what you suggested gave me idea for another approach. Basically it will avoid Copy and array allocation but not vec allocation. We will create vec's in the first place and use references for fill_bytes and encrypt_inplace, similar to what you suggest.

Since these vec's are fixed size pre-allocated, i think it should be fine?

Eh, it removes the stack allocations, which are essentially free, and the copy, yeah.

(Overall i think it shouldn't be a big concern since it is around 28bytes and we allocate much more during just a single gossip msg forward)

The larger concern is not the allocation size but rather any resulting heap fragmentation. I guess your argument is that in comparison to other parts of the code (e.g., gossip) this is trivial or at least similar, IIUC. Most gossip messages are indeed larger but are allocated largely on the stack. Not entirely though as they do contain a Vec field here and there (e.g., features), so I'd buy your argument that perhaps this isn't too much to worry about from a frequency of small allocation perspective.

(From my limited understanding of glibc allocator, the optimization achieved by using a single Storable as field is easily achieved at allocator level i think. Repeated small allocations will reuse the same small-bins/fast-bins/tcache. It might not avoid alloc traffic but those operations are meant to be fast. As long as number of small allocations don't keep on growing it is fine.)

I'm not knowledgable enough here to say what would happen in practice. Essentially, we'll have three small, fixed-size heap allocations for the Vecs and String, which are free'ed whenever the Storeable is freed. Presumably the caller will call encode immediately to pass the value to the KVStore. Afterwards, the Storeable is freed upon drop and thus so are the small allocations.

The question is, will the allocator reuse those small pieces of memory the next time a Storable is created? And will there be much heap fragmentation in the interim?

@TheBlueMatt I'm indifferent on keeping the proto. Seems it's not a whole lot different from other places in the code where there may be comparable allocation patterns (e.g., gossip forwarding). But let me know if I'm missing anything.

@G8XSUG8XSUNov 10, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Most gossip messages are indeed larger but are allocated largely on the stack.

No they are heap allocated, MessageBuf is just an abstraction over Vec link

I did a small instrumentation, for 2000,000 (2million) back to back 16-byte vector allocations, there are only 530 real allocations, 530 bins/slots that get re-used. (Those are 16byte mallocs again and again)

At no point in time did real memory cross those 530 alloc worth space. (so no frag risk i guess, even though there was alloc traffic)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No they are heap allocated, MessageBuf is just an abstraction over Vec link

FWIW, I was referring to the gossip messages after they have been deserialized from a MessageBuf. Those structs' fields are mostly (but not entirely) primitives or other structs that don't require heap allocation.

@G8XSU
G8XSU requested a review from jkczyzNovember 14, 2023 01:57

@jkczyzjkczyz left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Just some minor comments. Feel free to squash fixups and any changes addressing these comments.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
@G8XSU
G8XSU merged commit 3a26735 into lightningdevkit:mainNov 16, 2023
@G8XSUG8XSU mentioned this pull request Nov 28, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@G8XSU@jkczyz@tnull
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add storable_builder helper for client side encryption - #14

Merged
G8XSU merged 2 commits into
lightningdevkit:mainfrom
G8XSU:crypto
Nov 16, 2023
Merged

Add storable_builder helper for client side encryption #14
G8XSU merged 2 commits into
lightningdevkit:mainfrom
G8XSU:crypto

Conversation

@G8XSU

@G8XSUG8XSU commented Nov 2, 2023

Copy link
Copy Markdown
Contributor
  • Add storable_builder helper for client side encryption

  • Note: All of ChaCha20Poly1305 code is copied from rust-lightning repo. (I deleted code which was specifically added for LDK)

  • Note: decrypt_in_place function didn't exist in chacha20poly1305, it is a new addition, reviewers should review it.

@G8XSU
G8XSUforce-pushed the crypto branch 2 times, most recently from b68759d to 8d14179CompareNovember 2, 2023 00:29
@G8XSU
G8XSU requested a review from jkczyzNovember 2, 2023 00:29
@G8XSU
G8XSU marked this pull request as draft November 2, 2023 00:51
@G8XSU
G8XSUforce-pushed the crypto branch 2 times, most recently from 513f7d3 to 3017e9aCompareNovember 2, 2023 01:11
@G8XSU
G8XSU marked this pull request as ready for review November 2, 2023 01:19
@G8XSUG8XSU changed the title Add Storable_Builder helper for client side encryption Add storable_builder helper for client side encryption Nov 2, 2023

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So far just had a look at the first commit. Generally checked that the files generally match (mod reformatting), but have two questions regarding the differences.

self.mac.raw_result(out_tag);
}

pub fn encrypt_inplace(&mut self, input_output: &mut [u8], out_tag: &mut [u8]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new addition seems to be identical to the existing encrypt_full_message_in_place. Can we keep the existing name, so that we eventually know they are the same method when migrating back to rust-lightning?

@G8XSUG8XSUNov 3, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

i found encrypt_in_place more applicable than encrypt_full_message_in_place (i think message here refers to peer-to-peer msgs)
But i am ok with changing it.
This one is not a new addition, only decrypt_inplace is new addition.

Comment threadsrc/crypto/chacha20poly1305.rs
@jkczyz

Copy link
Copy Markdown
  • Note: All of ChaCha20Poly1305 code is copied from rust-lightning repo. (I deleted code which was specifically added for LDK)
  • Note: decrypt_in_place function didn't exist in chacha20poly1305, it is a new addition, reviewers should review it.

Pardon if this has already discussed, but is it worth refactoring code into a new crate to avoid copying? (cc: @TheBlueMatt)

We had a similar issue with some code shared across crates in the rust-lightning repo, but we were able to use symlinking there. Unfortunately, the same can't be done across repos.

@G8XSU

G8XSU commented Nov 3, 2023

Copy link
Copy Markdown
ContributorAuthor

@jkczyz

  1. We can't really depend on rust-lightning repo/crate for chacha20 encryption.

I did discuss it with Matt and Elias earlier,
if we don't want to copy code we can consider adding a dependency on some encryption library.
But it was argued that it is just 1 / 2-file and we can copy it. (and since it should never change, it is ok)

@tnull

tnull commented Nov 6, 2023

Copy link
Copy Markdown
Contributor

FWIW, lightningdevkit/rust-lightning#2708 will add the decrypt_in_place functionality upstream, so yo should be good just copy/pasting the files from upstream once that is merged. This has the benefit that you can just drop them entirely again when the VSS client itself is upstreamed.

@jkczyz

Copy link
Copy Markdown

FWIW, lightningdevkit/rust-lightning#2708 will add the decrypt_in_place functionality upstream, so yo should be good just copy/pasting the files from upstream once that is merged. This has the benefit that you can just drop them entirely again when the VSS client itself is upstreamed.

Oh, I wasn't aware that we were upstreaming this. Do you mean the entire vss-rust-client repo or something else?

@tnull

tnull commented Nov 6, 2023

Copy link
Copy Markdown
Contributor

Oh, I wasn't aware that we were upstreaming this. Do you mean the entire vss-rust-client repo or something else?

I thought there were plans around doing that at least eventually, i.e. have VssStore be part of rust-lightning? Or maybe I'm mistaken?

@G8XSU

G8XSU commented Nov 6, 2023

Copy link
Copy Markdown
ContributorAuthor

Yup, I just saw that we are upstreaming decrypt_in_place. :)

vss-rust-client will not be upstreamed (this PR is vss-rust-client), VssStore will be upstreamed to rust-lightning eventually.

Comment threadCargo.toml Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +27 to +50
Storable {
data: data_blob,
encryption_metadata: Option::from(EncryptionMetadata {
nonce: nonce.to_vec(),
tag: tag.to_vec(),
cipher_format: CHACHA20_CIPHER_NAME.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this result essentially immediately serialized and written to disk? From an allocation perspective, it's a bit unfortunate we have an intermediary object requiring a few Vecs if the alternative is to use something like Writeable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

almost in most cases yes.
main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable.
others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

iiuc, even if it was writeable,
it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

almost in most cases yes. main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable. others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

Any Vec creation other than an empty one results in a heap allocation. So there are two when you use to_vec here and another heap allocation for to_string on CHACHA20_CIPHER_NAME.

iiuc, even if it was writeable, it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

If we didn't use Storable, then we could serialize nonce,tag, and CHACHA20_CIPHER_NAME without creating any new Vecs or Strings . True that it would ultimately put everything into a Vec, so at least one allocation is required.

If we used a rust struct implementing Writeable instead of a proto, we could avoid the unnecessary allocations.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +27 to +50
Storable {
data: data_blob,
encryption_metadata: Option::from(EncryptionMetadata {
nonce: nonce.to_vec(),
tag: tag.to_vec(),
cipher_format: CHACHA20_CIPHER_NAME.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

almost in most cases yes. main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable. others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

Any Vec creation other than an empty one results in a heap allocation. So there are two when you use to_vec here and another heap allocation for to_string on CHACHA20_CIPHER_NAME.

iiuc, even if it was writeable, it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

If we didn't use Storable, then we could serialize nonce,tag, and CHACHA20_CIPHER_NAME without creating any new Vecs or Strings . True that it would ultimately put everything into a Vec, so at least one allocation is required.

If we used a rust struct implementing Writeable instead of a proto, we could avoid the unnecessary allocations.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +40 to +41
nonce: nonce.to_vec(),
tag: tag.to_vec(),
nonce: Vec::from(nonce),
tag: Vec::from(tag),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These still result in heap allocations. A new array is created on the heap in Vec within a Box type and then the data is copied in because arrays implement Copy. Ultimately, the nonce and tag memory is already stack allocated.

Other than foregoing the use of the proto altogether or using an arena (if possible), the only way to avoid excessive heap allocations would be to allocate one Storable as a member of StorableBuilder and reuse its Vec fields by taking slices to use with fill_bytes and encrypt_inplace. Then you would only need to allocate one String for cipher_format, too. But the caller would need to also reuse StorableBuilder, and build couldn't return a Storable any more. Instead, the interface would be in terms of bytes and Storable would be an implementation detail.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Hmm.. yeah, missed the box::new inside of implementation.

Storable as a member of StorableBuilder

Yeah but that introduces a state which mutates per request, making it non-thread safe. I think it will complicate everything.

But what you suggested gave me idea for another approach.
Basically it will avoid Copy and array allocation but not vec allocation. We will create vec's in the first place and use references for fill_bytes and encrypt_inplace, similar to what you suggest.

Since these vec's are fixed size pre-allocated, i think it should be fine?

(Overall i think it shouldn't be a big concern since it is around 28bytes and we allocate much more during just a single gossip msg forward)

(From my limited understanding of glibc allocator, the optimization achieved by using a single Storable as field is easily achieved at allocator level i think. Repeated small allocations will reuse the same small-bins/fast-bins/tcache. It might not avoid alloc traffic but those operations are meant to be fast.
As long as number of small allocations don't keep on growing it is fine.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yeah but that introduces a state which mutates per request, making it non-thread safe. I think it will complicate everything.

You could use a Storable per thread, FWIW.

But what you suggested gave me idea for another approach. Basically it will avoid Copy and array allocation but not vec allocation. We will create vec's in the first place and use references for fill_bytes and encrypt_inplace, similar to what you suggest.

Since these vec's are fixed size pre-allocated, i think it should be fine?

Eh, it removes the stack allocations, which are essentially free, and the copy, yeah.

(Overall i think it shouldn't be a big concern since it is around 28bytes and we allocate much more during just a single gossip msg forward)

The larger concern is not the allocation size but rather any resulting heap fragmentation. I guess your argument is that in comparison to other parts of the code (e.g., gossip) this is trivial or at least similar, IIUC. Most gossip messages are indeed larger but are allocated largely on the stack. Not entirely though as they do contain a Vec field here and there (e.g., features), so I'd buy your argument that perhaps this isn't too much to worry about from a frequency of small allocation perspective.

(From my limited understanding of glibc allocator, the optimization achieved by using a single Storable as field is easily achieved at allocator level i think. Repeated small allocations will reuse the same small-bins/fast-bins/tcache. It might not avoid alloc traffic but those operations are meant to be fast. As long as number of small allocations don't keep on growing it is fine.)

I'm not knowledgable enough here to say what would happen in practice. Essentially, we'll have three small, fixed-size heap allocations for the Vecs and String, which are free'ed whenever the Storeable is freed. Presumably the caller will call encode immediately to pass the value to the KVStore. Afterwards, the Storeable is freed upon drop and thus so are the small allocations.

The question is, will the allocator reuse those small pieces of memory the next time a Storable is created? And will there be much heap fragmentation in the interim?

@TheBlueMatt I'm indifferent on keeping the proto. Seems it's not a whole lot different from other places in the code where there may be comparable allocation patterns (e.g., gossip forwarding). But let me know if I'm missing anything.

@G8XSUG8XSUNov 10, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Most gossip messages are indeed larger but are allocated largely on the stack.

No they are heap allocated, MessageBuf is just an abstraction over Vec link

I did a small instrumentation, for 2000,000 (2million) back to back 16-byte vector allocations, there are only 530 real allocations, 530 bins/slots that get re-used. (Those are 16byte mallocs again and again)

At no point in time did real memory cross those 530 alloc worth space. (so no frag risk i guess, even though there was alloc traffic)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No they are heap allocated, MessageBuf is just an abstraction over Vec link

FWIW, I was referring to the gossip messages after they have been deserialized from a MessageBuf. Those structs' fields are mostly (but not entirely) primitives or other structs that don't require heap allocation.

@G8XSU
G8XSU requested a review from jkczyzNovember 14, 2023 01:57

@jkczyzjkczyz left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Just some minor comments. Feel free to squash fixups and any changes addressing these comments.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
@G8XSU
G8XSU merged commit 3a26735 into lightningdevkit:mainNov 16, 2023
@G8XSUG8XSU mentioned this pull request Nov 28, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@G8XSU@jkczyz@tnull
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add storable_builder helper for client side encryption - #14

Merged
G8XSU merged 2 commits into
lightningdevkit:mainfrom
G8XSU:crypto
Nov 16, 2023
Merged

Add storable_builder helper for client side encryption #14
G8XSU merged 2 commits into
lightningdevkit:mainfrom
G8XSU:crypto

Conversation

@G8XSU

@G8XSUG8XSU commented Nov 2, 2023

Copy link
Copy Markdown
Contributor
  • Add storable_builder helper for client side encryption

  • Note: All of ChaCha20Poly1305 code is copied from rust-lightning repo. (I deleted code which was specifically added for LDK)

  • Note: decrypt_in_place function didn't exist in chacha20poly1305, it is a new addition, reviewers should review it.

@G8XSU
G8XSUforce-pushed the crypto branch 2 times, most recently from b68759d to 8d14179CompareNovember 2, 2023 00:29
@G8XSU
G8XSU requested a review from jkczyzNovember 2, 2023 00:29
@G8XSU
G8XSU marked this pull request as draft November 2, 2023 00:51
@G8XSU
G8XSUforce-pushed the crypto branch 2 times, most recently from 513f7d3 to 3017e9aCompareNovember 2, 2023 01:11
@G8XSU
G8XSU marked this pull request as ready for review November 2, 2023 01:19
@G8XSUG8XSU changed the title Add Storable_Builder helper for client side encryption Add storable_builder helper for client side encryption Nov 2, 2023

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So far just had a look at the first commit. Generally checked that the files generally match (mod reformatting), but have two questions regarding the differences.

self.mac.raw_result(out_tag);
}

pub fn encrypt_inplace(&mut self, input_output: &mut [u8], out_tag: &mut [u8]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new addition seems to be identical to the existing encrypt_full_message_in_place. Can we keep the existing name, so that we eventually know they are the same method when migrating back to rust-lightning?

@G8XSUG8XSUNov 3, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

i found encrypt_in_place more applicable than encrypt_full_message_in_place (i think message here refers to peer-to-peer msgs)
But i am ok with changing it.
This one is not a new addition, only decrypt_inplace is new addition.

Comment threadsrc/crypto/chacha20poly1305.rs
@jkczyz

Copy link
Copy Markdown
  • Note: All of ChaCha20Poly1305 code is copied from rust-lightning repo. (I deleted code which was specifically added for LDK)
  • Note: decrypt_in_place function didn't exist in chacha20poly1305, it is a new addition, reviewers should review it.

Pardon if this has already discussed, but is it worth refactoring code into a new crate to avoid copying? (cc: @TheBlueMatt)

We had a similar issue with some code shared across crates in the rust-lightning repo, but we were able to use symlinking there. Unfortunately, the same can't be done across repos.

@G8XSU

G8XSU commented Nov 3, 2023

Copy link
Copy Markdown
ContributorAuthor

@jkczyz

  1. We can't really depend on rust-lightning repo/crate for chacha20 encryption.

I did discuss it with Matt and Elias earlier,
if we don't want to copy code we can consider adding a dependency on some encryption library.
But it was argued that it is just 1 / 2-file and we can copy it. (and since it should never change, it is ok)

@tnull

tnull commented Nov 6, 2023

Copy link
Copy Markdown
Contributor

FWIW, lightningdevkit/rust-lightning#2708 will add the decrypt_in_place functionality upstream, so yo should be good just copy/pasting the files from upstream once that is merged. This has the benefit that you can just drop them entirely again when the VSS client itself is upstreamed.

@jkczyz

Copy link
Copy Markdown

FWIW, lightningdevkit/rust-lightning#2708 will add the decrypt_in_place functionality upstream, so yo should be good just copy/pasting the files from upstream once that is merged. This has the benefit that you can just drop them entirely again when the VSS client itself is upstreamed.

Oh, I wasn't aware that we were upstreaming this. Do you mean the entire vss-rust-client repo or something else?

@tnull

tnull commented Nov 6, 2023

Copy link
Copy Markdown
Contributor

Oh, I wasn't aware that we were upstreaming this. Do you mean the entire vss-rust-client repo or something else?

I thought there were plans around doing that at least eventually, i.e. have VssStore be part of rust-lightning? Or maybe I'm mistaken?

@G8XSU

G8XSU commented Nov 6, 2023

Copy link
Copy Markdown
ContributorAuthor

Yup, I just saw that we are upstreaming decrypt_in_place. :)

vss-rust-client will not be upstreamed (this PR is vss-rust-client), VssStore will be upstreamed to rust-lightning eventually.

Comment threadCargo.toml Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +27 to +50
Storable {
data: data_blob,
encryption_metadata: Option::from(EncryptionMetadata {
nonce: nonce.to_vec(),
tag: tag.to_vec(),
cipher_format: CHACHA20_CIPHER_NAME.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this result essentially immediately serialized and written to disk? From an allocation perspective, it's a bit unfortunate we have an intermediary object requiring a few Vecs if the alternative is to use something like Writeable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

almost in most cases yes.
main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable.
others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

iiuc, even if it was writeable,
it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

almost in most cases yes. main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable. others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

Any Vec creation other than an empty one results in a heap allocation. So there are two when you use to_vec here and another heap allocation for to_string on CHACHA20_CIPHER_NAME.

iiuc, even if it was writeable, it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

If we didn't use Storable, then we could serialize nonce,tag, and CHACHA20_CIPHER_NAME without creating any new Vecs or Strings . True that it would ultimately put everything into a Vec, so at least one allocation is required.

If we used a rust struct implementing Writeable instead of a proto, we could avoid the unnecessary allocations.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +27 to +50
Storable {
data: data_blob,
encryption_metadata: Option::from(EncryptionMetadata {
nonce: nonce.to_vec(),
tag: tag.to_vec(),
cipher_format: CHACHA20_CIPHER_NAME.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

almost in most cases yes. main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable. others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

Any Vec creation other than an empty one results in a heap allocation. So there are two when you use to_vec here and another heap allocation for to_string on CHACHA20_CIPHER_NAME.

iiuc, even if it was writeable, it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

If we didn't use Storable, then we could serialize nonce,tag, and CHACHA20_CIPHER_NAME without creating any new Vecs or Strings . True that it would ultimately put everything into a Vec, so at least one allocation is required.

If we used a rust struct implementing Writeable instead of a proto, we could avoid the unnecessary allocations.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +40 to +41
nonce: nonce.to_vec(),
tag: tag.to_vec(),
nonce: Vec::from(nonce),
tag: Vec::from(tag),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These still result in heap allocations. A new array is created on the heap in Vec within a Box type and then the data is copied in because arrays implement Copy. Ultimately, the nonce and tag memory is already stack allocated.

Other than foregoing the use of the proto altogether or using an arena (if possible), the only way to avoid excessive heap allocations would be to allocate one Storable as a member of StorableBuilder and reuse its Vec fields by taking slices to use with fill_bytes and encrypt_inplace. Then you would only need to allocate one String for cipher_format, too. But the caller would need to also reuse StorableBuilder, and build couldn't return a Storable any more. Instead, the interface would be in terms of bytes and Storable would be an implementation detail.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Hmm.. yeah, missed the box::new inside of implementation.

Storable as a member of StorableBuilder

Yeah but that introduces a state which mutates per request, making it non-thread safe. I think it will complicate everything.

But what you suggested gave me idea for another approach.
Basically it will avoid Copy and array allocation but not vec allocation. We will create vec's in the first place and use references for fill_bytes and encrypt_inplace, similar to what you suggest.

Since these vec's are fixed size pre-allocated, i think it should be fine?

(Overall i think it shouldn't be a big concern since it is around 28bytes and we allocate much more during just a single gossip msg forward)

(From my limited understanding of glibc allocator, the optimization achieved by using a single Storable as field is easily achieved at allocator level i think. Repeated small allocations will reuse the same small-bins/fast-bins/tcache. It might not avoid alloc traffic but those operations are meant to be fast.
As long as number of small allocations don't keep on growing it is fine.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yeah but that introduces a state which mutates per request, making it non-thread safe. I think it will complicate everything.

You could use a Storable per thread, FWIW.

But what you suggested gave me idea for another approach. Basically it will avoid Copy and array allocation but not vec allocation. We will create vec's in the first place and use references for fill_bytes and encrypt_inplace, similar to what you suggest.

Since these vec's are fixed size pre-allocated, i think it should be fine?

Eh, it removes the stack allocations, which are essentially free, and the copy, yeah.

(Overall i think it shouldn't be a big concern since it is around 28bytes and we allocate much more during just a single gossip msg forward)

The larger concern is not the allocation size but rather any resulting heap fragmentation. I guess your argument is that in comparison to other parts of the code (e.g., gossip) this is trivial or at least similar, IIUC. Most gossip messages are indeed larger but are allocated largely on the stack. Not entirely though as they do contain a Vec field here and there (e.g., features), so I'd buy your argument that perhaps this isn't too much to worry about from a frequency of small allocation perspective.

(From my limited understanding of glibc allocator, the optimization achieved by using a single Storable as field is easily achieved at allocator level i think. Repeated small allocations will reuse the same small-bins/fast-bins/tcache. It might not avoid alloc traffic but those operations are meant to be fast. As long as number of small allocations don't keep on growing it is fine.)

I'm not knowledgable enough here to say what would happen in practice. Essentially, we'll have three small, fixed-size heap allocations for the Vecs and String, which are free'ed whenever the Storeable is freed. Presumably the caller will call encode immediately to pass the value to the KVStore. Afterwards, the Storeable is freed upon drop and thus so are the small allocations.

The question is, will the allocator reuse those small pieces of memory the next time a Storable is created? And will there be much heap fragmentation in the interim?

@TheBlueMatt I'm indifferent on keeping the proto. Seems it's not a whole lot different from other places in the code where there may be comparable allocation patterns (e.g., gossip forwarding). But let me know if I'm missing anything.

@G8XSUG8XSUNov 10, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Most gossip messages are indeed larger but are allocated largely on the stack.

No they are heap allocated, MessageBuf is just an abstraction over Vec link

I did a small instrumentation, for 2000,000 (2million) back to back 16-byte vector allocations, there are only 530 real allocations, 530 bins/slots that get re-used. (Those are 16byte mallocs again and again)

At no point in time did real memory cross those 530 alloc worth space. (so no frag risk i guess, even though there was alloc traffic)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No they are heap allocated, MessageBuf is just an abstraction over Vec link

FWIW, I was referring to the gossip messages after they have been deserialized from a MessageBuf. Those structs' fields are mostly (but not entirely) primitives or other structs that don't require heap allocation.

@G8XSU
G8XSU requested a review from jkczyzNovember 14, 2023 01:57

@jkczyzjkczyz left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Just some minor comments. Feel free to squash fixups and any changes addressing these comments.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
@G8XSU
G8XSU merged commit 3a26735 into lightningdevkit:mainNov 16, 2023
@G8XSUG8XSU mentioned this pull request Nov 28, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@G8XSU@jkczyz@tnull
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add storable_builder helper for client side encryption - #14

Merged
G8XSU merged 2 commits into
lightningdevkit:mainfrom
G8XSU:crypto
Nov 16, 2023
Merged

Add storable_builder helper for client side encryption #14
G8XSU merged 2 commits into
lightningdevkit:mainfrom
G8XSU:crypto

Conversation

@G8XSU

@G8XSUG8XSU commented Nov 2, 2023

Copy link
Copy Markdown
Contributor
  • Add storable_builder helper for client side encryption

  • Note: All of ChaCha20Poly1305 code is copied from rust-lightning repo. (I deleted code which was specifically added for LDK)

  • Note: decrypt_in_place function didn't exist in chacha20poly1305, it is a new addition, reviewers should review it.

@G8XSU
G8XSUforce-pushed the crypto branch 2 times, most recently from b68759d to 8d14179CompareNovember 2, 2023 00:29
@G8XSU
G8XSU requested a review from jkczyzNovember 2, 2023 00:29
@G8XSU
G8XSU marked this pull request as draft November 2, 2023 00:51
@G8XSU
G8XSUforce-pushed the crypto branch 2 times, most recently from 513f7d3 to 3017e9aCompareNovember 2, 2023 01:11
@G8XSU
G8XSU marked this pull request as ready for review November 2, 2023 01:19
@G8XSUG8XSU changed the title Add Storable_Builder helper for client side encryption Add storable_builder helper for client side encryption Nov 2, 2023

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So far just had a look at the first commit. Generally checked that the files generally match (mod reformatting), but have two questions regarding the differences.

self.mac.raw_result(out_tag);
}

pub fn encrypt_inplace(&mut self, input_output: &mut [u8], out_tag: &mut [u8]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new addition seems to be identical to the existing encrypt_full_message_in_place. Can we keep the existing name, so that we eventually know they are the same method when migrating back to rust-lightning?

@G8XSUG8XSUNov 3, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

i found encrypt_in_place more applicable than encrypt_full_message_in_place (i think message here refers to peer-to-peer msgs)
But i am ok with changing it.
This one is not a new addition, only decrypt_inplace is new addition.

Comment threadsrc/crypto/chacha20poly1305.rs
@jkczyz

Copy link
Copy Markdown
  • Note: All of ChaCha20Poly1305 code is copied from rust-lightning repo. (I deleted code which was specifically added for LDK)
  • Note: decrypt_in_place function didn't exist in chacha20poly1305, it is a new addition, reviewers should review it.

Pardon if this has already discussed, but is it worth refactoring code into a new crate to avoid copying? (cc: @TheBlueMatt)

We had a similar issue with some code shared across crates in the rust-lightning repo, but we were able to use symlinking there. Unfortunately, the same can't be done across repos.

@G8XSU

G8XSU commented Nov 3, 2023

Copy link
Copy Markdown
ContributorAuthor

@jkczyz

  1. We can't really depend on rust-lightning repo/crate for chacha20 encryption.

I did discuss it with Matt and Elias earlier,
if we don't want to copy code we can consider adding a dependency on some encryption library.
But it was argued that it is just 1 / 2-file and we can copy it. (and since it should never change, it is ok)

@tnull

tnull commented Nov 6, 2023

Copy link
Copy Markdown
Contributor

FWIW, lightningdevkit/rust-lightning#2708 will add the decrypt_in_place functionality upstream, so yo should be good just copy/pasting the files from upstream once that is merged. This has the benefit that you can just drop them entirely again when the VSS client itself is upstreamed.

@jkczyz

Copy link
Copy Markdown

FWIW, lightningdevkit/rust-lightning#2708 will add the decrypt_in_place functionality upstream, so yo should be good just copy/pasting the files from upstream once that is merged. This has the benefit that you can just drop them entirely again when the VSS client itself is upstreamed.

Oh, I wasn't aware that we were upstreaming this. Do you mean the entire vss-rust-client repo or something else?

@tnull

tnull commented Nov 6, 2023

Copy link
Copy Markdown
Contributor

Oh, I wasn't aware that we were upstreaming this. Do you mean the entire vss-rust-client repo or something else?

I thought there were plans around doing that at least eventually, i.e. have VssStore be part of rust-lightning? Or maybe I'm mistaken?

@G8XSU

G8XSU commented Nov 6, 2023

Copy link
Copy Markdown
ContributorAuthor

Yup, I just saw that we are upstreaming decrypt_in_place. :)

vss-rust-client will not be upstreamed (this PR is vss-rust-client), VssStore will be upstreamed to rust-lightning eventually.

Comment threadCargo.toml Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +27 to +50
Storable {
data: data_blob,
encryption_metadata: Option::from(EncryptionMetadata {
nonce: nonce.to_vec(),
tag: tag.to_vec(),
cipher_format: CHACHA20_CIPHER_NAME.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this result essentially immediately serialized and written to disk? From an allocation perspective, it's a bit unfortunate we have an intermediary object requiring a few Vecs if the alternative is to use something like Writeable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

almost in most cases yes.
main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable.
others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

iiuc, even if it was writeable,
it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

almost in most cases yes. main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable. others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

Any Vec creation other than an empty one results in a heap allocation. So there are two when you use to_vec here and another heap allocation for to_string on CHACHA20_CIPHER_NAME.

iiuc, even if it was writeable, it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

If we didn't use Storable, then we could serialize nonce,tag, and CHACHA20_CIPHER_NAME without creating any new Vecs or Strings . True that it would ultimately put everything into a Vec, so at least one allocation is required.

If we used a rust struct implementing Writeable instead of a proto, we could avoid the unnecessary allocations.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +27 to +50
Storable {
data: data_blob,
encryption_metadata: Option::from(EncryptionMetadata {
nonce: nonce.to_vec(),
tag: tag.to_vec(),
cipher_format: CHACHA20_CIPHER_NAME.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

almost in most cases yes. main constituent of it is data_blob, which is encrypted_in_place and decrypted_in_place, and then used as it is in storable. others are just 96bytes and 16bytes. from allocation perspective, i don't think it will allocate unless we encode it. it will just hold references to it.

Any Vec creation other than an empty one results in a heap allocation. So there are two when you use to_vec here and another heap allocation for to_string on CHACHA20_CIPHER_NAME.

iiuc, even if it was writeable, it would be serializing vec's one after other into one big VecWriter just before writing when encode() is called.

If we didn't use Storable, then we could serialize nonce,tag, and CHACHA20_CIPHER_NAME without creating any new Vecs or Strings . True that it would ultimately put everything into a Vec, so at least one allocation is required.

If we used a rust struct implementing Writeable instead of a proto, we could avoid the unnecessary allocations.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment on lines +40 to +41
nonce: nonce.to_vec(),
tag: tag.to_vec(),
nonce: Vec::from(nonce),
tag: Vec::from(tag),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These still result in heap allocations. A new array is created on the heap in Vec within a Box type and then the data is copied in because arrays implement Copy. Ultimately, the nonce and tag memory is already stack allocated.

Other than foregoing the use of the proto altogether or using an arena (if possible), the only way to avoid excessive heap allocations would be to allocate one Storable as a member of StorableBuilder and reuse its Vec fields by taking slices to use with fill_bytes and encrypt_inplace. Then you would only need to allocate one String for cipher_format, too. But the caller would need to also reuse StorableBuilder, and build couldn't return a Storable any more. Instead, the interface would be in terms of bytes and Storable would be an implementation detail.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Hmm.. yeah, missed the box::new inside of implementation.

Storable as a member of StorableBuilder

Yeah but that introduces a state which mutates per request, making it non-thread safe. I think it will complicate everything.

But what you suggested gave me idea for another approach.
Basically it will avoid Copy and array allocation but not vec allocation. We will create vec's in the first place and use references for fill_bytes and encrypt_inplace, similar to what you suggest.

Since these vec's are fixed size pre-allocated, i think it should be fine?

(Overall i think it shouldn't be a big concern since it is around 28bytes and we allocate much more during just a single gossip msg forward)

(From my limited understanding of glibc allocator, the optimization achieved by using a single Storable as field is easily achieved at allocator level i think. Repeated small allocations will reuse the same small-bins/fast-bins/tcache. It might not avoid alloc traffic but those operations are meant to be fast.
As long as number of small allocations don't keep on growing it is fine.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yeah but that introduces a state which mutates per request, making it non-thread safe. I think it will complicate everything.

You could use a Storable per thread, FWIW.

But what you suggested gave me idea for another approach. Basically it will avoid Copy and array allocation but not vec allocation. We will create vec's in the first place and use references for fill_bytes and encrypt_inplace, similar to what you suggest.

Since these vec's are fixed size pre-allocated, i think it should be fine?

Eh, it removes the stack allocations, which are essentially free, and the copy, yeah.

(Overall i think it shouldn't be a big concern since it is around 28bytes and we allocate much more during just a single gossip msg forward)

The larger concern is not the allocation size but rather any resulting heap fragmentation. I guess your argument is that in comparison to other parts of the code (e.g., gossip) this is trivial or at least similar, IIUC. Most gossip messages are indeed larger but are allocated largely on the stack. Not entirely though as they do contain a Vec field here and there (e.g., features), so I'd buy your argument that perhaps this isn't too much to worry about from a frequency of small allocation perspective.

(From my limited understanding of glibc allocator, the optimization achieved by using a single Storable as field is easily achieved at allocator level i think. Repeated small allocations will reuse the same small-bins/fast-bins/tcache. It might not avoid alloc traffic but those operations are meant to be fast. As long as number of small allocations don't keep on growing it is fine.)

I'm not knowledgable enough here to say what would happen in practice. Essentially, we'll have three small, fixed-size heap allocations for the Vecs and String, which are free'ed whenever the Storeable is freed. Presumably the caller will call encode immediately to pass the value to the KVStore. Afterwards, the Storeable is freed upon drop and thus so are the small allocations.

The question is, will the allocator reuse those small pieces of memory the next time a Storable is created? And will there be much heap fragmentation in the interim?

@TheBlueMatt I'm indifferent on keeping the proto. Seems it's not a whole lot different from other places in the code where there may be comparable allocation patterns (e.g., gossip forwarding). But let me know if I'm missing anything.

@G8XSUG8XSUNov 10, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Most gossip messages are indeed larger but are allocated largely on the stack.

No they are heap allocated, MessageBuf is just an abstraction over Vec link

I did a small instrumentation, for 2000,000 (2million) back to back 16-byte vector allocations, there are only 530 real allocations, 530 bins/slots that get re-used. (Those are 16byte mallocs again and again)

At no point in time did real memory cross those 530 alloc worth space. (so no frag risk i guess, even though there was alloc traffic)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No they are heap allocated, MessageBuf is just an abstraction over Vec link

FWIW, I was referring to the gossip messages after they have been deserialized from a MessageBuf. Those structs' fields are mostly (but not entirely) primitives or other structs that don't require heap allocation.

@G8XSU
G8XSU requested a review from jkczyzNovember 14, 2023 01:57

@jkczyzjkczyz left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Just some minor comments. Feel free to squash fixups and any changes addressing these comments.

Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
Comment threadsrc/util/storable_builder.rs Outdated
@G8XSU
G8XSU merged commit 3a26735 into lightningdevkit:mainNov 16, 2023
@G8XSUG8XSU mentioned this pull request Nov 28, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@G8XSU@jkczyz@tnull