HTTP-based block source clients #774
Conversation
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.
Codecov Report
@@ Coverage Diff @@
## main #774 +/- ##
==========================================
- Coverage 91.35% 91.24% -0.12%
==========================================
Files 37 37
Lines 22791 22846 +55
==========================================
+ Hits 20821 20845 +24
- Misses 1970 2001 +31
Continue to review full report at Codecov.
|
|
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\ |
TheBlueMatt
Jan 13, 2021
Member
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.
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.
jkczyz
Jan 15, 2021
Author
Collaborator
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.
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.
| /// 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. |
valentinewallace
Jan 15, 2021
Collaborator
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)?
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)?
jkczyz
Jan 15, 2021
Author
Collaborator
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.
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.
TheBlueMatt
Jan 15, 2021
Member
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?
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?
jkczyz
Jan 16, 2021
Author
Collaborator
The multiple-source case is handled in ChainMultiplexer, which uses a set of ChainPollers (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.
The multiple-source case is handled in ChainMultiplexer, which uses a set of ChainPollers (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.
| [dependencies] | ||
| bitcoin = "0.24" | ||
| lightning = { version = "0.0.12", path = "../lightning" } | ||
| tokio = { version = ">=0.2.12", features = [ "tcp", "io-util", "dns" ], optional = true } |
valentinewallace
Jan 15, 2021
Collaborator
tokio doesn't seem like the most stable, any reason to not just pin to a specific version?
tokio doesn't seem like the most stable, any reason to not just pin to a specific version?
jkczyz
Jan 15, 2021
Author
Collaborator
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.
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.
TheBlueMatt
Jan 15, 2021
Member
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.
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.
jkczyz
Jan 16, 2021
Author
Collaborator
Updated tokio to 1.0. However, I was unable to get it working with lightning-net-tokio. The test would hang during connection setup.
rust-lightning/lightning-net-tokio/src/lib.rs
Lines 626 to 631
in
d529a88
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?
Updated tokio to 1.0. However, I was unable to get it working with lightning-net-tokio. The test would hang during connection setup.
rust-lightning/lightning-net-tokio/src/lib.rs
Lines 626 to 631 in d529a88
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?
| } | ||
| } | ||
|
|
||
| /// Response data from `getblockheader` RPC and `headers` REST requests. |
valentinewallace
Jan 15, 2021
Collaborator
It'd be cool to have CI to spin up a bitcoind node and test these. Maybe too much for this PR though.
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
| } | ||
| } | ||
|
|
||
| #[tokio::test] |
valentinewallace
Jan 18, 2021
Collaborator
Could maybe add a success test case for the non-tokio version.
Could maybe add a success test case for the non-tokio version.
jkczyz
Jan 19, 2021
Author
Collaborator
IIUC, this is using the tokio runtime to drive the test. Otherwise, it will fail when running the test with the tokio feature.
---- http::client_tests::connect_with_valid_endpoint stdout ----
thread 'http::client_tests::connect_with_valid_endpoint' panicked at 'there is no reactor running, must be called from the context of Tokio runtime', /Users/jkczyz/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.0.2/src/io/driver/mod.rs:262:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
However, the non-tokio version is tested still as long as the tokio feature is not specified when running the test.
IIUC, this is using the tokio runtime to drive the test. Otherwise, it will fail when running the test with the tokio feature.
---- http::client_tests::connect_with_valid_endpoint stdout ----
thread 'http::client_tests::connect_with_valid_endpoint' panicked at 'there is no reactor running, must be called from the context of Tokio runtime', /Users/jkczyz/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.0.2/src/io/driver/mod.rs:262:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
However, the non-tokio version is tested still as long as the tokio feature is not specified when running the test.
| if: matrix.build-net-tokio | ||
| run: | | ||
| cd lightning-block-sync | ||
| RUSTFLAGS="-C link-dead-code" cargo build --verbose --color always --features rest-client |
valentinewallace
Jan 18, 2021
Collaborator
Should the tests be run at some point? I could be missing something but I can't find them in CI output... Also http::client_tests::connect_to_unresolvable_host's failing for me locally.
Should the tests be run at some point? I could be missing something but I can't find them in CI output... Also http::client_tests::connect_to_unresolvable_host's failing for me locally.
jkczyz
Jan 19, 2021
Author
Collaborator
Should the tests be run at some point? I could be missing something but I can't find them in CI output...
Whoops! That also may be why there's coverage failures. :P
Also http::client_tests::connect_to_unresolvable_host's failing for me locally.
Are you seeing "Expected error" or something else?
Should the tests be run at some point? I could be missing something but I can't find them in CI output...
Whoops! That also may be why there's coverage failures. :P
Also
http::client_tests::connect_to_unresolvable_host's failing for me locally.
Are you seeing "Expected error" or something else?
valentinewallace
Jan 19, 2021
Collaborator
Are you seeing "Expected error" or something else?
Hmm it seems to be passing now, weird. I'll let you know if I can reproduce it again but it's probably my mistake.
Are you seeing "Expected error" or something else?
Hmm it seems to be passing now, weird. I'll let you know if I can reproduce it again but it's probably my mistake.
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.
| /// | ||
| /// 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> |
valentinewallace
Jan 19, 2021
•
Collaborator
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.
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.
jkczyz
Jan 20, 2021
Author
Collaborator
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.
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
postrequests?
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.
valentinewallace
Jan 20, 2021
Collaborator
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.
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.
| } | ||
|
|
||
| /// Creates an endpoint using the HTTPS scheme. | ||
| pub fn secure_host(host: String) -> Self { |
valentinewallace
Jan 19, 2021
Collaborator
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).
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).
jkczyz
Jan 20, 2021
Author
Collaborator
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.
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.
valentinewallace
Jan 20, 2021
Collaborator
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?
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?
| if: matrix.build-net-tokio | ||
| run: | | ||
| cd lightning-block-sync | ||
| RUSTFLAGS="-C link-dead-code" cargo build --verbose --color always --features rest-client |
valentinewallace
Jan 19, 2021
Collaborator
Are you seeing "Expected error" or something else?
Hmm it seems to be passing now, weird. I'll let you know if I can reproduce it again but it's probably my mistake.
Are you seeing "Expected error" or something else?
Hmm it seems to be passing now, weird. I'll let you know if I can reproduce it again but it's probably my mistake.
|
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 |
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. |
aa13ad0
to
4e092b3
Windows is giving AddrNotAvailable instead of ConnectionRefused. Using an IPv4 address (0.0.0.0) didn't make a difference.
Interprets HTTP responses as either binary or JSON format, which are then converted to the appropriate data types.
|
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). |
| } | ||
|
|
||
| // TODO: Refactor HttpClient to support TLS. | ||
| #[cfg(test)] |
valentinewallace
Jan 20, 2021
Collaborator
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).
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).
| //! 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. |
valentinewallace
Jan 20, 2021
Collaborator
As discussed offline, these docs could use an update for the pros/cons + more details of enabling tokio feature.
As discussed offline, these docs could use an update for the pros/cons + more details of enabling tokio feature.
| return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected JSON result")); | ||
| } | ||
|
|
||
| JsonResponse(result.take()).try_into() |
valentinewallace
Jan 20, 2021
Collaborator
As discussed offline, could add a test for the try_into() here.
As discussed offline, could add a test for the try_into() here.
| /// | ||
| /// 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> |
valentinewallace
Jan 20, 2021
Collaborator
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.
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, |
valentinewallace
Jan 20, 2021
Collaborator
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.
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.
| basic_auth: "Basic ".to_string() + credentials, | ||
| endpoint, | ||
| client, | ||
| id: AtomicUsize::new(0), |
valentinewallace
Jan 20, 2021
Collaborator
Not sure if conclusions were reached, but we discussed how the ID wasn't being fully utilized in the current PRs and could potentially be pushed off.
Not sure if conclusions were reached, but we discussed how the ID wasn't being fully utilized in the current PRs and could potentially be pushed off.
| }); | ||
|
|
||
| let mut response = self.client.post::<JsonResponse>(&uri, &host, &self.basic_auth, content) | ||
| .await?.0; |
valentinewallace
Jan 20, 2021
•
Collaborator
I forget if there was any conclusion but the .0 was a bit mysterious / maybe a comment about the tuple it comes from could be nice.
I forget if there was any conclusion but the .0 was a bit mysterious / maybe a comment about the tuple it comes from could be nice.
| } | ||
|
|
||
| /// Creates an endpoint using the HTTPS scheme. | ||
| pub fn secure_host(host: String) -> Self { |
valentinewallace
Jan 20, 2021
Collaborator
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?
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?

Formed in 2009, the Archive Team (not to be confused with the archive.org Archive-It Team) is a rogue archivist collective dedicated to saving copies of rapidly dying or deleted websites for the sake of history and digital heritage. The group is 100% composed of volunteers and interested parties, and has expanded into a large amount of related projects for saving online and digital history.

Adds a
lightning-block-synccrate consisting of the following components:BlockSourcetrait for fetching blocks and headersBlockSourcetraitThis PR is the first half of #763.