Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grow noise buffers dynamically. #1436

Merged
merged 4 commits into from
Feb 13, 2020
Merged

Conversation

twittner
Copy link
Contributor

@twittner twittner commented Feb 6, 2020

Currently we allocate a buffer of 176 KiB for each noise state, i.e. each connection. For connections which see only small data frames this is wasteful. At the same time we limit the max. write buffer size to 16 KiB to keep the total buffer size relatively small, which results in smaller encrypted messages and also makes it less likely to ever encounter the max. noise package size of 64 KiB in practice when communicating with other nodes using the same implementation.

This PR replaces the static buffer allocation with a dynamic one. We only reserve a small space for the authentication tag plus some extra reserve and are able to buffer larger data frames before encrypting.

In addition the noise smoke tests have been changed to send a sequence of messages over the connection and not just one to better test the state handling. The messages themselves are also often larger now to test that the max. noise package size is not exceeded and write buffer handling works properly.

Currently we allocate a buffer of 176 KiB for each noise state, i.e.
each connection. For connections which see only small data frames
this is wasteful. At the same time we limit the max. write buffer size
to 16 KiB to keep the total buffer size relatively small, which
results in smaller encrypted messages and also makes it less likely to
ever encounter the max. noise package size of 64 KiB in practice when
communicating with other nodes using the same implementation.

This PR repaces the static buffer allocation with a dynamic one. We
only reserve a small space for the authentication tag plus some extra
reserve and are able to buffer larger data frames before encrypting.
@@ -443,7 +414,7 @@ fn read_frame_len<R: AsyncRead + Unpin>(
cx: &mut Context<'_>,
buf: &mut [u8; 2],
off: &mut usize,
) -> Poll<Result<Option<u16>, std::io::Error>> {
) -> Poll<io::Result<Option<u16>>> {
Copy link
Member

Choose a reason for hiding this comment

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

Commenting on this PR as well, because I comment every time someone does that: I'm not a fan of doing random codestyle changes, especially for code that isn't otherwise touched by the PR.

const MAX_WRITE_BUF_LEN: usize = 16384;
const TOTAL_BUFFER_LEN: usize = 2 * MAX_NOISE_PKG_LEN + 3 * MAX_WRITE_BUF_LEN;
/// Extra space given to the encryption buffer to hold key material.
const EXTRA_ENCRYPT_SPACE: usize = 1024;
Copy link
Member

Choose a reason for hiding this comment

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

That constant looks a bit like dark magic to me, but the test would cover any possible mistake here anyway.

Copy link
Member

@mxinden mxinden left a comment

Choose a reason for hiding this comment

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

Two questions (one out of curiosity, one covering an edge case) probably not worth blocking this pull request:

&buffer.read[.. len],
buffer.read_crypto
){
this.decrypt_buffer.resize(len, 0u8);
Copy link
Member

Choose a reason for hiding this comment

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

At this point len is the size of the encrypted message in this.read_buffer, correct? Do you resize this.decrypt_buffer to len to ensure it is large enough, given that the decrypted payload will always be smaller than the encrypted payload?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes.

loop {
trace!("write state: {:?}", this.write_state);
match this.write_state {
WriteState::Init => {
this.write_state = WriteState::BufferData { off: 0 }
}
WriteState::BufferData { ref mut off } => {
let n = std::cmp::min(MAX_WRITE_BUF_LEN - *off, buf.len());
buffer.write[*off .. *off + n].copy_from_slice(&buf[.. n]);
let n = min(MAX_WRITE_BUF_LEN, this.write_buffer.len().saturating_add(buf.len()));
Copy link
Member

Choose a reason for hiding this comment

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

Do I understand correctly, that we never set this.write_buffer back to 0. Let's say one always calls poll_write with a very small buf (e.g. length of 1). In addition let's say one always calls poll_flush right afterwards each time.

As far as I can tell this.write_buffer would eventually be as large as MAX_WRITE_BUF_LEN due to this.write_buffer.len().saturating_add(buf.len()) followed by this.write_buffer.resize(n, 0u8); even though one only ever needed this.write_buffer to be of length 1.

Would resizing this.write_buffer with the current offset (off) instead of this.write_buffer.len() solve this?

Suggested change
let n = min(MAX_WRITE_BUF_LEN, this.write_buffer.len().saturating_add(buf.len()));
let n = min(MAX_WRITE_BUF_LEN, off.saturating_add(buf.len()));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch!

As suggested by @mxinden, this prevents increasing the write buffer up
to MAX_WRITE_BUF_LEN.
Copy link
Member

@mxinden mxinden left a comment

Choose a reason for hiding this comment

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

This looks good to me. 👍 for improving the tests. Got one small test related comment. No need to block the pull request due to it.

@@ -40,7 +41,8 @@ fn core_upgrade_compat() {
#[test]
fn xx() {
let _ = env_logger::try_init();
fn prop(message: Vec<u8>) -> bool {
fn prop(mut messages: Vec<Message>) -> bool {
messages.truncate(5);
Copy link
Member

Choose a reason for hiding this comment

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

I am guessing that you are truncateing messages here to prevent huge messages vector, right?

Why not use TestResult::discard when messages is greater than 5. Would increase the entropy of the test as it would also run with 4, 3, 2 and 1 message.

See for comparison: https://github.com/libp2p/rust-libp2p/blob/master/protocols/gossipsub/tests/smoke.rs#L171

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am guessing that you are truncateing messages here to prevent huge messages vector, right?

Yes.

Why not use TestResult::discard when messages is greater than 5.

It is not so much the length that matters here, but just that multiple messages are transmitted. If generation is skewed towards the upper bound 5 it is alright. Truncating the input means less input is generated only to be discarded. That being said I am not opposed to change this to use TestResult::discard.

Would increase the entropy of the test as it would also run with 4, 3, 2 and 1 message.

The test runs just fine with vectors of length 0 to 4.

Copy link
Member

Choose a reason for hiding this comment

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

I am fine either way. In case you think this would be beneficial, add it, otherwise leave it as is with static 5.

@romanb romanb merged commit 70d634d into libp2p:master Feb 13, 2020
@twittner twittner deleted the noise-dynamic-buffers branch February 13, 2020 12:26
Sign up for free to 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.

4 participants