-
Notifications
You must be signed in to change notification settings - Fork 407
HTTP-based block source clients #774
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
Conversation
7d5d75e
to
5994c92
Compare
Codecov Report
@@ Coverage Diff @@
## main #774 +/- ##
==========================================
- Coverage 90.80% 90.71% -0.10%
==========================================
Files 38 44 +6
Lines 23168 24066 +898
==========================================
+ Hits 21038 21831 +793
- Misses 2130 2235 +105
Continue to review full report at Codecov.
|
5994c92
to
619f25c
Compare
FYI, the latest push split Not sure what's up with Clippy:
|
let request = format!( | ||
"GET {} HTTP/1.1\r\n\ | ||
Host: {}\r\n\ | ||
Connection: keep-alive\r\n\ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Connection: keep-alive
usually will only hold the connection for N requests (order 100), so we need some kind of logic to reconnect if write_request() fails once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had trouble getting a test to return an appropriate error from write_request
when closing the stream. I was able to get an unexpected EOF from read_response
though. I made the error check around both instead, reconnecting and retrying when detected.
BTW, the REST and RPC interfaces were creating new connections for each request. I updated them to maintain the connection instead.
lightning-block-sync/src/lib.rs
Outdated
/// Returns the hash of the best block and, optionally, its height. The height is passed to | ||
/// `get_header` to allow a more efficient lookup on some block sources. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Second sentence may be able to be clearer. Who's passing the height? RL?
Is get_header
's docs saying that if get_header
returns a Transient
error, then RL will try to get the height from get_best_block
and use it on get_header
(even though get_header
seemingly would take any height, not just the best one)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The forthcoming MicroSPVClient
would pass the height from either the result of calling get_best_block
or by computing it from a BlockHeaderData
.
I can write a better summary at the trait-level, but I think this raises an important point regarding the interface. Namely, in what case would a source not provide a height? If get_best_block
returns a None
height with the hash, how could a call to get_header
on the same source return a BlockHeaderData
for the hash given that it has a height
field to populate?
@TheBlueMatt Should we drop Option
from around the height in these two functions? Or at very least from get_best_block
? I suppose we may want to call get_header
without a hint if we deserialized a listener that only had the most recent block hash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, good question! I agree its maybe dubious to claim there will be an implementation that polls the best chain tip and only gets a hash, but its still somewhat nice to have - it wouldn't be unheard of, and requiring less data for the best block polling may leave some other things (like headers over radio) more easy to implement. I'd need to dig into the MicroSPVClient
, but is it not the case that height_hint
will never be None unless a source returned None for the height in get_best_block
(or is it the case that you can hit it if one source returns None but another source returns Some?)? That would simplify the docs somewhat and I'm not sure it generates a lot of code in the MicroSPVClient
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The multiple-source case is handled in ChainMultiplexer
, which uses a set of ChainPoller
s (one for each block source). So the result of get_best_block
is never fed to get_header
of a different source. So, yeah, I think the docs can be simplified a bit with this invariant in mind.
But note that relatedly, as it stands, get_header
must return BlockHeaderData
containing a height. If it were changed to be Option
, this would prevent the fork detection logic from functioning. And more importantly, a height is eventually needed to pass to block_connected
and block_disconnected
.
IIUC, then, a headers-only source must return the height along with the header. Were you suggesting that it may be easier to implement such a source if this were not the case? We could conceivably make the height field an Option
, but it would complicate the code quite a bit. Trying different source would be predicated on this field being None
rather than on whether there was an error. I suppose that could be abstracted away to ChainMultiplexer
if needed. Just want to make sure I understand the use case.
lightning-block-sync/Cargo.toml
Outdated
[dependencies] | ||
bitcoin = "0.24" | ||
lightning = { version = "0.0.12", path = "../lightning" } | ||
tokio = { version = ">=0.2.12", features = [ "tcp", "io-util", "dns" ], optional = true } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tokio
doesn't seem like the most stable, any reason to not just pin to a specific version?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kept it matching lightning-net-tokio
but did run into some trouble with dependency version inconsistency when needing serde
later on.
@TheBlueMatt Was 0.2.12 chosen for a reason? Presumably there is some bug fix in there. Should we pin on that? I think ">=0.2.12"
won't bump us up to 0.3.0 at very least, IIUC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This matches what we use in lightning-net-tokio which is important. That said, we should just upgrade both to 1.0 and move on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated tokio to 1.0. However, I was unable to get it working with lightning-net-tokio
. The test would hang during connection setup.
Initial investigation seemed to show it stuck waiting on tokio::select!
. I tried both 1.0 and 0.3 (the public beta for 1.0) and had the same results.
Any insight?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, decided to revert tokio 1.0 support since it required using Rust 1.45 which led to other issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like a tokio bug, I filed one upstream. tokio-rs/tokio#3474 For now I guess lets use 0.2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that the failure in threaded mode is separate - it seems tokio also no longer closes connections when you call close so the other thread doesn't learn the connection is gone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nevermind, they set me straight quick.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | ||
} | ||
|
||
/// Response data from `getblockheader` RPC and `headers` REST requests. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be cool to have CI to spin up a bitcoind node and test these. Maybe too much for this PR though.
5046f08
to
80db689
Compare
acbe5af
to
208d7d6
Compare
/// | ||
/// The request body consists of the provided JSON `content`. Returns the response body in `F` | ||
/// format. | ||
pub async fn post<F>(&mut self, uri: &str, host: &str, auth: &str, content: serde_json::Value) -> std::io::Result<F> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we wanted to save some LoC, doesn't look like this is used and I don't think Core has any API that takes post
requests? Edit: there's also a warning in CI output because it's unused.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we wanted to save some LoC, doesn't look like this is used and I don't think Core has any API that takes
post
requests?
This is used by RpcClient::call_method
as the client uses the HTTP request body to specify the RPC using JSON.
Edit: there's also a warning in CI output because it's unused.
I did see some warnings in intermediary commits but not from HEAD. Looks like this is because the methods aren't used until the implementations of BlockSource
are added in a later commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is used by RpcClient::call_method as the client uses the HTTP request body to specify the RPC using JSON.
Missed this!
As discussed, there still seem to be some warnings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. They were coming up when either the rpc-client
or rest-client
feature was enabled but not when both were.
lightning-block-sync/src/http.rs
Outdated
} | ||
|
||
/// Creates an endpoint using the HTTPS scheme. | ||
pub fn secure_host(host: String) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not 100% sure but I think supporting HTTPS/TLS may need extra work and could be put off.
For example:
#[tokio::test]
async fn test_https_servers() {
let endpoint = HttpEndpoint::secure_host("www.facebook.com".to_string());
let mut client = HttpClient::connect(&endpoint).unwrap();
let res = client.get::<BinaryResponse>("/", "facebook.com").await.unwrap();
}
Seems to result in read_line!
reading something binary that isn't what curl
returns, so I think the TLS isn't being handled? Also other HTTP client implementations seem to be doing extra work for TLS (e.g. https://docs.rs/native-tls/0.2.7/native_tls/#examples).
If I'm off here, I still think it could be worth adding a test for both HTTP and HTTPS clients using google.com
(as opposed to just the test HttpServer).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not 100% sure but I think supporting HTTPS/TLS may need extra work and could be put off.
Ah, no, you're right. Thanks for testing this case! I remember running across this awhile ago but it fell off my radar. I'll just remove support for it and add a TODO. It would require refactoring the HttpClient
API a bit.
If I'm off here, I still think it could be worth adding a test for both HTTP and HTTPS clients using
google.com
(as opposed to just the test HttpServer).
Generally, I'd prefer to make unit tests hermetic whenever possible to avoid flakiness with the external world.
Having separate tests would be useful, though. I would add a placeholder now to demonstrate that it's currently not supported, but that isn't possible with the current API taking E: ToSocketAddrs
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally, I'd prefer to make unit tests hermetic whenever possible to avoid flakiness with the external world.
I've come to agree with this, like we wouldn't want to require an internet connection to run our tests.
Having separate tests would be useful, though. I would add a placeholder now to demonstrate that it's currently not supported, but that isn't possible with the current API taking E: ToSocketAddrs.
Hmm should've asked in the call but I wasn't sure what this meant. Separate tests for what? and what does "placeholder" mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm should've asked in the call but I wasn't sure what this meant. Separate tests for what? and what does "placeholder" mean?
More precisely a "placeholder test". Essentially, a test for HttpClient:connect
that demonstrate that it fails when given a secure endpoint. Once HTTPS support is added, the test could be updated to check for success instead.
However, this won't work (compile) for the reason that I gave.
I think I partly figured out why coverage is failing --
and the reason it can't find any coverage reports is because in
which only looks |
208d7d6
to
66e7cbd
Compare
Yeah, I think you're on to something. It may be related to moving to Rust 1.45, which Tokio 1.0 requires. I removed that commit. Let's see if it helps any. |
4e092b3
to
7c8c68a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is shaping up from my POV!
One minor thing would be to note somewhere what versions of Bitcoin Core this would work for (not crucial for this PR though).
lightning-block-sync/src/http.rs
Outdated
} | ||
|
||
// TODO: Refactor HttpClient to support TLS. | ||
#[cfg(test)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit in favor of just ripping all of the TLS-related out and adding all of it later when we can actually support it (e.g. the presence of Scheme
could be confusing for future readers who then assume we support HTTPS).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed. It would probably make more sense to separate this anyhow.
//! Both features support either blocking I/O using `std::net::TcpStream` or, with feature `tokio`, | ||
//! non-blocking I/O using `tokio::net::TcpStream` from inside a Tokio runtime. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed offline, these docs could use an update for the pros/cons + more details of enabling tokio
feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may have added to the confusion during our discussion. For the non-tokio case, there is never an await
on stream I/O since the stream used (std::net::TcpStream
) has a synchronous interface. Therefore, stream I/O will block the thread.
For the tokio case, tokio::net::TcpStream
has an asynchronous interface (i.e., it requires calling await
on the returned future). Whenever steam I/O is hit, the runtime is given the chance to schedule other work if blocking would have occurred. Therefore, stream I/O will not block the thread.
So while the overall BlockSource
interface is asynchronous (i.e., written in terms of async
functions), HttpClient
is only non-blocking when using the tokio
feature. But either way a runtime is needed to drive the execution.
Let me know if that cleared anything up and if you have any suggestions for making the docs more intuitive.
/// | ||
/// The request body consists of the provided JSON `content`. Returns the response body in `F` | ||
/// format. | ||
pub async fn post<F>(&mut self, uri: &str, host: &str, auth: &str, content: serde_json::Value) -> std::io::Result<F> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is used by RpcClient::call_method as the client uses the HTTP request body to specify the RPC using JSON.
Missed this!
As discussed, there still seem to be some warnings.
Persistent, | ||
|
||
/// Indicates an error that may resolve when retrying a request (e.g., unresponsive). | ||
Transient, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my testing, frequently I wanted to know what the actual error was (like EOF) and added print statements to retrieve it. So, might be slightly nicer if Transient
contained the actual error. Not a big deal though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Made this a wrapper around an underlying error similar to std::io::Error
.
lightning-block-sync/src/http.rs
Outdated
} | ||
|
||
/// Creates an endpoint using the HTTPS scheme. | ||
pub fn secure_host(host: String) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally, I'd prefer to make unit tests hermetic whenever possible to avoid flakiness with the external world.
I've come to agree with this, like we wouldn't want to require an internet connection to run our tests.
Having separate tests would be useful, though. I would add a placeholder now to demonstrate that it's currently not supported, but that isn't possible with the current API taking E: ToSocketAddrs.
Hmm should've asked in the call but I wasn't sure what this meant. Separate tests for what? and what does "placeholder" mean?
d771419
to
8eec576
Compare
Rustc 1.45 moved the paths to test binaries, so we need to update our CI scripts to run the correct ones under kcov. The solution to this was pointed out by Val at lightningdevkit#774 (comment)
} | ||
} | ||
|
||
#[cfg(test)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you extract some test-cases from a real bitcoin node response to the various queries used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's essentially what I'm doing here, primarily with the genesis block and generated programmatically (e.g. line 170) to ensure the tests are concise. I could add missing fields that are irrelevant to the conversion, but I don't think that adds much value to the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I meant using the full set of queries used, eg including GetHeaderResponse
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... not sure I understand. I believe that I covered all the queries. Did I miss something?
get_header:
- into_block_header_from_json_response_with_valid_header
- into_block_header_from_json_response_with_valid_header_array
- into_block_header_from_json_response_without_previous_block_hash
get_block
- into_block_from_valid_binary_response
- into_block_from_json_response_with_valid_block_data
get_best_block
- into_block_hash_from_json_response_without_height
- into_block_hash_from_json_response_with_height
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I was looking for a string copied from the wire off wireshark or the like, but I suppose creating them by filling in objects manually is fine enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For unit tests, I prefer to keep the inputs as concise as possible yet still representative. Using raw wire data would be reasonable for an integration test, IMHO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There seems to be a (spurious?) failure on stable: https://i.imgur.com/jASWBwD.png
There may be others, didn't check. Also linting failing for some reason.
I'm good to approve after those fixes!
8eec576
to
329d6cc
Compare
Ugh, thought I had resolved this but apparently not. Added a timeout in 9871d40 when using
Looks like the same problem as earlier: #774 (comment) Found others having the same issue but need to delve into it further: rust-lang/rust-clippy#3874 |
Turns out this doesn't seem to help. :( Tried implementing @TheBlueMatt I'm only ever seeing this with |
795b047
to
2895102
Compare
Ended up removing the
Fixed the linter error by rebasing on #781 since it moves stable to 1.45.2. IIUC, clippy runs over all dependencies which may have their own configs. One of them was likely incompatible with the previous stable. |
2895102
to
09b3f12
Compare
Forgot that this only works if the server closes the connection. Managed to get an upstream change that enable a workaround. Now we can read asynchronously and use |
Defines an interface and related types for fetching block headers and data from a block source (e.g., Bitcoin Core). Used to keep lightning in sync with chain activity.
09b3f12
to
0c31383
Compare
Rebased and squashed fixup commits. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All super trivial notes.
lightning-block-sync/Cargo.toml
Outdated
serde = { version = "1.0", features = ["derive"], optional = true } | ||
serde_json = { version = "1.0", optional = true } | ||
chunked_transfer = { version = "1.4.0", optional = true } | ||
futures = { version = "0.3.8" } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor nit: I think just write 0.3? Looks like its already using 0.3.12 with this text, though, so it doesn't matter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there was some dependency mismatch earlier which seemed to be fixed by specifying it this way. But it may have been my confusion. Changed to 0.3 as that works now.
/// persistent errors. | ||
pub struct BlockSourceError { | ||
kind: BlockSourceErrorKind, | ||
error: Box<dyn std::error::Error + Send + Sync>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a blocker, but I'm not really sure the value of this - with a dyn Error
the only thing you can really do with it is string-format it, and a fixed, templated version would make mixing different block sources have type errors. I need to take another look at the next PR, however, to see how its used, and we can discuss more there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I largely followed the pattern used by std::io::Error
, which this error happens to wrap in this PR. That error also implements std::error::Error
which allows examiners to follow the chain of errors. I chose not to implement that here for sake of simplicity, but leaving it as is gives us the option of adding it later.
In the follow-up, this error wraps either a string or any errors emanating from rust-bitcoin when validating block data, IIRC. Happy to discuss it more in that PR if necessary.
/// Endpoint for interacting with an HTTP-based API. | ||
#[derive(Debug)] | ||
pub struct HttpEndpoint { | ||
host: String, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There aren't consistency requirements, so why not just make all the fields pub
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Partly it's this way as a result of the evolution of the struct. That said, not making these pub
has some advantages. Once constructed it is immutable from the user's perspective. We can also vary the implementation in the future without worrying about clients. It also keeps the interface consistent given port
is optional and path
has a default. They would require accessor methods or special initialization to maintain the same behavior.
|
||
// Read message body | ||
let read_limit = MAX_HTTP_MESSAGE_BODY_SIZE - reader.buffer().len(); | ||
reader.get_mut().set_limit(read_limit as u64); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the chunked encoding case, we're limiting to 4MB including chunked encoding overhead, which I guess isn't ideal - maybe we should just bump MAX_HTTP_MESSAGE_BODY_SIZE
to 2*4MB + 32KB or something larger so that its clearly fine?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Updated as recommended.
lightning-block-sync/src/http.rs
Outdated
None => break, | ||
Some(chunk_size) => chunk_size, | ||
}; | ||
let mut chunk_body = vec![0; chunk_size + "\r\n".len()]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can avoid the extra allocations by writing into content.
- let mut chunk_body = vec![0; chunk_size + "\r\n".len()];
- reader.read_exact(&mut chunk_body[..]).await?;
- content.extend_from_slice(&chunk_body[..chunk_size]);
+ let orig_len = content.len();
+ content.resize(orig_len + chunk_size + "\r\n".len(), 0);
+ reader.read_exact(&mut content[orig_len..]).await?;
+ content.resize(orig_len + chunk_size, 0);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@valentinewallace had mentioned she's happy once the test hangs are fixed, so up to you when you want it merged @jkczyz. |
0c31383
to
34fe4ae
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you squash the fixups?
Implements a simple HTTP client that can issue GET and POST requests. Used to implement REST and RPC clients, respectively. Both clients support either blocking or non-blocking I/O.
Interprets HTTP responses as either binary or JSON format, which are then converted to the appropriate data types.
34fe4ae
to
85fdfaa
Compare
Adds a
lightning-block-sync
crate consisting of the following components:BlockSource
trait for fetching blocks and headersBlockSource
traitThis PR is the first half of #763.