Skip to content

Conversation

@0xB10C
Copy link
Contributor

@0xB10C 0xB10C commented Jan 8, 2026

The first commit fixes #159 and the second adds a test. To test the fix, revert the first commit and run the test:

cargo test --test round_trips pingpong_with_closed_connection_async --features tokio

I haven't tested if the sync code works. Would probably be good to extend the tests for this too.

When read() returns with zero bytes read, this indicates an EOF.
Previously, we'd continue to read() in a loop and never return
to the caller if that happend. Fix this by returning a ProtocolError
with ErrorKind::ConnectionAborted "read zero bytes". This results in
an ProtocolFailureSuggestion::Abort.

fixes rust-bitcoin#159
let len = self.reader.read(&mut length_bytes[*bytes_read..]).await?;
if len == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::ConnectionAborted,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Went with ConnectionAborted here. ConnectionReset is an alternative. Happy to be convinced to use ConnectionReset if someone has strong feelings about it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

No strong opinion here, ConnectionAborted is fine with me.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we use UnexpectedEof? It looks like that is what tokio bubbles up in similar scenarios.

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 thought about this too, but found the Rust stdlib docs to not to be a perfect fit for our case? https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.UnexpectedEof

An error returned when an operation could not be completed because an “end of file” was reached prematurely.
This typically means that an operation could only succeed if it read a particular number of bytes but only a smaller number of bytes could be read.

(but also don't really care too much personally)

println!(
"Trying to read another message from the server, while the connection is already closed."
);
assert!(protocol.read().await.is_err());
Copy link
Contributor Author

@0xB10C 0xB10C Jan 8, 2026

Choose a reason for hiding this comment

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

Hm the test still passed when async writing to the closed protocol. I think that should return an error too? At least it doesn't hang..

index 5b96eca..e7cb5d1 100644
--- a/protocol/tests/round_trips.rs
+++ b/protocol/tests/round_trips.rs
@@ -229,6 +229,10 @@ async fn pingpong_with_closed_connection_async() {
         "Trying to read another message from the server, while the connection is already closed."
     );
     assert!(protocol.read().await.is_err());
+
+    let ping = V2NetworkMessage::new(NetworkMessage::Ping(45324));
+    let message = consensus::serialize(&ping);
+    protocol.write(&Payload::genuine(message)).await.unwrap();
 }

 #[test]

Probably a good followup once this is merged.

@rustaceanrob
Copy link
Collaborator

rustaceanrob commented Jan 8, 2026

From the tokio docs

To shut down the stream in the write direction, you can call the shutdown() method. This will cause the other peer to receive a read of length 0, indicating that no more data will be sent. This only closes the stream in one direction.

It appears we are receiving said 0 and then slamming the CPU in the while loop. This solution works. Alternatively, is there a reason to not use read_exact here? My impression is that call will return with EOF if they close as well.

cc @nyonson

@nyonson
Copy link
Collaborator

nyonson commented Jan 8, 2026

Alternatively, is there a reason to not use read_exact here?

I'd like to keep the functions cancellation safe if possible (read_exact isn't so would break that).

@rustaceanrob
Copy link
Collaborator

That's right. In that case I think this approach works. Haven't review the test yet.

@nyonson
Copy link
Collaborator

nyonson commented Jan 8, 2026

@0xB10C thanks for the report + fix! Hoping to give this library some more attention once we wrap up the consensus_encoding work in rust-bitcoin.

This adds a test for the bug described in
rust-bitcoin#159
and fixed in the previous commit.

The test starts a ping-pong server that only respondes to one Ping(x)
with a Pong(x) and then stops. Further async protocol read()'s should
now error.

Addtionally, also test that writing a message to closed protocol fails.
@0xB10C 0xB10C force-pushed the 2026-01-fix-reads-on-closed-socket branch from 4019a67 to 0719bcc Compare January 8, 2026 20:24
@0xB10C
Copy link
Contributor Author

0xB10C commented Jan 8, 2026

From the tokio docs

To shut down the stream in the write direction, you can call the shutdown() method. This will cause the other peer to receive a read of length 0, indicating that no more data will be sent. This only closes the stream in one direction.

It appears we are receiving said 0 and then slamming the CPU in the while loop. This solution works.

fwiw I think these are the relevant docs for the read from AsyncReadExt that are used here: https://docs.rs/tokio/latest/tokio/io/trait.AsyncReadExt.html#method.read

Return

If the return value of this method is Ok(n), then it must be guaranteed that 0 <= n <= buf.len(). A nonzero n value indicates that the buffer buf has been filled in with n bytes of data from this source. If n is 0, then it can indicate one of two scenarios:

  • This reader has reached its “end of file” and will likely no longer be able to produce bytes. Note that this does not mean that the reader will always no longer be able to produce bytes.
  • The buffer specified was 0 bytes in length.

(this sounds like UnexpectedEof is indeed the right error here)

Copy link
Collaborator

@nyonson nyonson left a comment

Choose a reason for hiding this comment

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

ACK 0719bcc

@rustaceanrob
Copy link
Collaborator

LGTM

@rustaceanrob rustaceanrob merged commit a989a87 into rust-bitcoin:main Jan 9, 2026
15 of 16 checks passed
@0xB10C
Copy link
Contributor Author

0xB10C commented Jan 9, 2026

I haven't tested if the sync code works. Would probably be good to extend the tests for this too.

In the sync code, read() seems to work. I'll PR a test for it. However, write() on a closed socket seems to not error too as in #161

0xB10C added a commit to 0xB10C/bip324 that referenced this pull request Jan 9, 2026
This implements the sync variant of 0719bcc.
While this works at the moment, I think it's good to have this test for
extra coverage. It can also be easily extended in a follow up to test
sync write() on a closed socket (see rust-bitcoin#160 (comment)).
0xB10C added a commit to 0xB10C/bip324 that referenced this pull request Jan 9, 2026
This implements the sync variant of 0719bcc.
While this works at the moment, I think it's good to have this test for
extra coverage. It can also be easily extended in a follow up to test
sync write() on a closed socket (see rust-bitcoin#160 (comment)).
@0xB10C 0xB10C deleted the 2026-01-fix-reads-on-closed-socket branch January 9, 2026 14:40
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.

async protocol.read().await does not return when underlying connection is closed

3 participants