The Wayback Machine - https://web.archive.org/web/20210121180115/https://github.com/rust-bitcoin/rust-lightning/pull/774
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

HTTP-based block source clients #774

Open
wants to merge 18 commits into
base: main
from

Conversation

@jkczyz
Copy link
Collaborator

@jkczyz jkczyz commented Jan 12, 2021

Adds a lightning-block-sync crate consisting of the following components:

  • BlockSource trait for fetching blocks and headers
  • HTTP-based REST and RPC clients implementing the BlockSource trait

This PR is the first half of #763.

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.
@jkczyz jkczyz requested a review from valentinewallace Jan 12, 2021
@jkczyz jkczyz force-pushed the jkczyz:2021-01-http-client branch from 7d5d75e to 5994c92 Jan 13, 2021
@codecov
Copy link

@codecov codecov bot commented Jan 13, 2021

Codecov Report

Merging #774 (30103be) into main (9c9c881) will decrease coverage by 0.11%.
The diff coverage is 92.12%.

Impacted file tree graph

@@            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     
Impacted Files Coverage Δ
lightning/src/lib.rs 100.00% <ø> (ø)
lightning/src/util/config.rs 50.00% <50.00%> (-19.05%) ⬇️
lightning/src/chain/keysinterface.rs 93.29% <62.50%> (-0.90%) ⬇️
lightning/src/util/test_utils.rs 83.68% <66.66%> (-1.16%) ⬇️
lightning/src/ln/channel.rs 87.49% <86.95%> (-0.03%) ⬇️
lightning/src/ln/onchaintx.rs 94.02% <96.61%> (+0.24%) ⬆️
lightning/src/chain/channelmonitor.rs 95.81% <100.00%> (+0.22%) ⬆️
lightning/src/ln/chanmon_update_fail_tests.rs 97.56% <100.00%> (ø)
lightning/src/ln/channelmanager.rs 84.94% <100.00%> (ø)
lightning/src/ln/functional_test_utils.rs 95.13% <100.00%> (ø)
... and 5 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 9c9c881...7c8c68a. Read the comment docs.

@jkczyz jkczyz force-pushed the jkczyz:2021-01-http-client branch from 5994c92 to 619f25c Jan 13, 2021
@jkczyz
Copy link
Collaborator Author

@jkczyz jkczyz commented Jan 13, 2021

FYI, the latest push split http_clients into http, rest, rpc, and convert. It also moved the contents of http_endpoint into http. Hope this separation makes the code easier to review.

Not sure what's up with Clippy:

error: error reading Clippy's configuration file `/home/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/pin-project-lite-0.2.4/.clippy.toml`: unknown field `msrv`, expected one of `blacklisted-names`, `cognitive-complexity-threshold`, `cyclomatic-complexity-threshold`, `doc-valid-idents`, `too-many-arguments-threshold`, `type-complexity-threshold`, `single-char-binding-names-threshold`, `too-large-for-stack`, `enum-variant-name-threshold`, `enum-variant-size-threshold`, `verbose-bit-mask-threshold`, `literal-representation-threshold`, `trivial-copy-size-limit`, `too-many-lines-threshold`, `third-party` at line 1 column 1
lightning-block-sync/src/rpc.rs Outdated Show resolved Hide resolved
let request = format!(
"GET {} HTTP/1.1\r\n\
Host: {}\r\n\
Connection: keep-alive\r\n\

This comment has been minimized.

@TheBlueMatt

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.

This comment has been minimized.

@jkczyz

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.

lightning-block-sync/src/http.rs Show resolved Hide resolved
lightning-block-sync/src/convert.rs Show resolved Hide resolved
lightning-block-sync/src/lib.rs Outdated Show resolved Hide resolved
/// 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.
Comment on lines 30 to 31

This comment has been minimized.

@valentinewallace

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)?

This comment has been minimized.

@jkczyz

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.

This comment has been minimized.

@TheBlueMatt

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?

This comment has been minimized.

@jkczyz

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.

[dependencies]
bitcoin = "0.24"
lightning = { version = "0.0.12", path = "../lightning" }
tokio = { version = ">=0.2.12", features = [ "tcp", "io-util", "dns" ], optional = true }

This comment has been minimized.

@valentinewallace

valentinewallace Jan 15, 2021
Collaborator

tokio doesn't seem like the most stable, any reason to not just pin to a specific version?

This comment has been minimized.

@jkczyz

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.

This comment has been minimized.

@TheBlueMatt

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 comment has been minimized.

@jkczyz

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.

let (sender, _receiver) = mpsc::channel(2);
let fut_a = super::setup_outbound(Arc::clone(&a_manager), sender.clone(), b_pub, tokio::net::TcpStream::from_std(conn_a).unwrap());
let fut_b = super::setup_inbound(b_manager, sender, tokio::net::TcpStream::from_std(conn_b).unwrap());
tokio::time::timeout(Duration::from_secs(10), a_connected.recv()).await.unwrap();
tokio::time::timeout(Duration::from_secs(1), b_connected.recv()).await.unwrap();

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?

lightning-block-sync/src/rest.rs Outdated Show resolved Hide resolved
}
}

/// Response data from `getblockheader` RPC and `headers` REST requests.

This comment has been minimized.

@valentinewallace

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.

@jkczyz jkczyz force-pushed the jkczyz:2021-01-http-client branch 4 times, most recently from 5046f08 to 80db689 Jan 15, 2021
lightning-block-sync/src/http.rs Outdated Show resolved Hide resolved
}
}

#[tokio::test]

This comment has been minimized.

@valentinewallace

valentinewallace Jan 18, 2021
Collaborator

Could maybe add a success test case for the non-tokio version.

This comment has been minimized.

@jkczyz

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.

lightning-block-sync/src/http.rs Show resolved Hide resolved
if: matrix.build-net-tokio
run: |
cd lightning-block-sync
RUSTFLAGS="-C link-dead-code" cargo build --verbose --color always --features rest-client

This comment has been minimized.

@valentinewallace

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.

This comment has been minimized.

@jkczyz

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?

This comment has been minimized.

@valentinewallace

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.

lightning-block-sync/src/rest.rs Show resolved Hide resolved
jkczyz added 10 commits Jan 19, 2021
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.
@jkczyz jkczyz force-pushed the jkczyz:2021-01-http-client branch from 80db689 to acbe5af Jan 19, 2021
@jkczyz jkczyz force-pushed the jkczyz:2021-01-http-client branch from acbe5af to 208d7d6 Jan 19, 2021
///
/// 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>

This comment has been minimized.

@valentinewallace

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.

This comment has been minimized.

@jkczyz

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.

This comment has been minimized.

@valentinewallace

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.

lightning-block-sync/src/http.rs Show resolved Hide resolved
lightning-block-sync/src/http.rs Outdated Show resolved Hide resolved
}

/// Creates an endpoint using the HTTPS scheme.
pub fn secure_host(host: String) -> Self {

This comment has been minimized.

@valentinewallace

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).

This comment has been minimized.

@jkczyz

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.

This comment has been minimized.

@valentinewallace

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?

if: matrix.build-net-tokio
run: |
cd lightning-block-sync
RUSTFLAGS="-C link-dead-code" cargo build --verbose --color always --features rest-client

This comment has been minimized.

@valentinewallace

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.

@valentinewallace
Copy link
Collaborator

@valentinewallace valentinewallace commented Jan 20, 2021

I think I partly figured out why coverage is failing --
So the failure happens with this CI output:

 ==> Searching for coverage reports in:
    + .
--> No coverage report found.

and the reason it can't find any coverage reports is because in .github/workflows/build.yml, there's this code that generates the coverage reports:

          for file in target/debug/lightning-*; do
            [ -x "${file}" ] || continue;
            mkdir -p "target/cov/$(basename $file)";
            ./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file";
          done

which only looks /target/debug/. But for some reason, the lightning-block-sync tests put their build artifacts in /target/debug/deps (resulting in no coverage reports being generated). So presumably the superficial fix would be to run that snippet^ in the additional directory. However I don't know why those files are in this different place. Our current setup may be a bit of a hack given that: https://doc.rust-lang.org/cargo/guide/build-cache.html says that the layout of these directories is subject to change.

@jkczyz jkczyz force-pushed the jkczyz:2021-01-http-client branch from 208d7d6 to 66e7cbd Jan 20, 2021
@jkczyz
Copy link
Collaborator Author

@jkczyz jkczyz commented Jan 20, 2021

I think I partly figured out why coverage is failing --
So the failure happens with this CI output:

 ==> Searching for coverage reports in:
    + .
--> No coverage report found.

and the reason it can't find any coverage reports is because in .github/workflows/build.yml, there's this code that generates the coverage reports:

          for file in target/debug/lightning-*; do
            [ -x "${file}" ] || continue;
            mkdir -p "target/cov/$(basename $file)";
            ./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file";
          done

which only looks /target/debug/. But for some reason, the lightning-block-sync tests put their build artifacts in /target/debug/deps (resulting in no coverage reports being generated). So presumably the superficial fix would be to run that snippet^ in the additional directory. However I don't know why those files are in this different place. Our current setup may be a bit of a hack given that: https://doc.rust-lang.org/cargo/guide/build-cache.html says that the layout of these directories is subject to change.

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.

@jkczyz jkczyz force-pushed the jkczyz:2021-01-http-client branch 2 times, most recently from aa13ad0 to 4e092b3 Jan 20, 2021
jkczyz added 3 commits Jan 20, 2021
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.
@jkczyz jkczyz force-pushed the jkczyz:2021-01-http-client branch from 4e092b3 to 7c8c68a Jan 20, 2021
Copy link
Collaborator

@valentinewallace valentinewallace left a comment

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)]

This comment has been minimized.

@valentinewallace

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).

//! 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.
Comment on lines +9 to +10

This comment has been minimized.

@valentinewallace

valentinewallace Jan 20, 2021
Collaborator

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()

This comment has been minimized.

@valentinewallace

valentinewallace Jan 20, 2021
Collaborator

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>

This comment has been minimized.

@valentinewallace

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.

Persistent,

/// Indicates an error that may resolve when retrying a request (e.g., unresponsive).
Transient,

This comment has been minimized.

@valentinewallace

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.

basic_auth: "Basic ".to_string() + credentials,
endpoint,
client,
id: AtomicUsize::new(0),

This comment has been minimized.

@valentinewallace

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.

});

let mut response = self.client.post::<JsonResponse>(&uri, &host, &self.basic_auth, content)
.await?.0;

This comment has been minimized.

@valentinewallace

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.

}

/// Creates an endpoint using the HTTPS scheme.
pub fn secure_host(host: String) -> Self {

This comment has been minimized.

@valentinewallace

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
3 participants