-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Add local Bitcoin node configuration detection #3982
Add local Bitcoin node configuration detection #3982
Conversation
Refactors LocalBitcoinNode and adds detection for local Bitcoin node's configuration, namely, whether it is pruning and whether it has bloom filter queries enabled. The local node's configuration (and its presence) is retrieved by performing a Bitcoin protocol handshake, which includes the local Bitcoin node sending us its version message (VersionMessage in BitcoinJ), which contains the information we're interested in. Due to some quirky BitcoinJ logic, sometimes the handshake is interrupted, even though we have received the local node's version message. That contributes to the handshake handling in LocalBitcoinNode being a bit complicated. Refactoring consists of two principle changes: the public interface is split into methods that trigger checks and methods that retrieve the cached results. The methods that trigger checks have names starting with "check", and methods that retrieve the cached results have names that start with "is". The other major refactor is the use of Optional<Boolean> instead of boolean for storing and returning the results, an empty Optional signifying that the relevant check was not yet performed. Switching to Optionals has caused other code that queries LocalBitcoinNode to throw an exception in case the query is made before the checks are. Before, the results were instantiated to "false" and that would be returned in case the query was made before the checks completed. This change has revealed one occasion (Preferences class) where this happens.
@@ -559,7 +576,7 @@ else if (displayTorNetworkSettingsHandler != null) | |||
|
|||
// We only init wallet service here if not using Tor for bitcoinj. | |||
// When using Tor, wallet init must be deferred until Tor is ready. | |||
if (!preferences.getUseTorForBitcoinJ() || localBitcoinNode.isDetected()) { |
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 redundant LocalBitcoinNode
call, because Preferences.getUseTorForBitcoinJ
makes the same call and it has the same effect.
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 localBitcoinNode.willUse() == true
and config.useTorForBtcOptionSetExplicitly == true
the new code will behave differently than before. So I don't think we should remove this from here.
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're right, I missed that. Thanks! My concern here is that this line decides whether to "initiate wallet" at this time, which implies that Tor will not be used. Ideally Preferences.getUseTorForBitcoinJ()
would be a full authority on whether or not we're using Tor, but here it's only one half of the condition. So I can restore initial logic, but ideally I'd like to restructure this. I'll see what I can do.
@@ -441,10 +451,12 @@ private void setupBtcNumPeersWatcher() { | |||
checkNumberOfBtcPeersTimer = UserThread.runAfter(() -> { | |||
// check again numPeers | |||
if (walletsSetup.numPeersProperty().get() == 0) { | |||
if (localBitcoinNode.isDetected()) | |||
getWalletServiceErrorMsg().set(Res.get("mainView.networkWarning.localhostBitcoinLost", Res.getBaseCurrencyName().toLowerCase())); |
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.
Apart from the .isDetected()
-> .isUsable.get()
line, these are just formatting changes.
// To keep the method's behaviour unchanged, until a fix is implemented, we use | ||
// Optional.orElse(false). Here 'false' normally means that the checks were performed | ||
// and a suitable local Bitcoin node wasn't found. | ||
var usableLocalNodePresent = localBitcoinNode.isUsable().orElse(false); |
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 the bug I mentioned.
@dmos62 Both travis and codacy fail. I will test this locally once it's building ok. |
I was erroniously targeting Java 11, when Travis requires Java 10. `Optional.isEmpty` shows up only in Java 11.
The workings of LocalBitcoinNode significantly changed, especially how detection works. Before, we were only checking if a port was open, but now we're actually performing a Bitcoin protocol handshake, which is difficult to stub. For these reasons the old tests are irrelevant and replacement tests were not written.
The implementation relies on BitcoinJ's NioClient attempting a handshake, but if that fails (for example when there isn't a locally running Bitcoin node), NioClient logs errors and exception to console, which is a bit annoying since we're already properly handling each case. Here's a screenshot: We could get the relevant BitcoinJ's logger and suppress it while we're doing these checks, but that feels a bit hacky (or does it?). Tell me how you feel about that. |
I have made some changes to satisfy one of two Codacy complaints, and to make the other clearer. The other is a future callback that does nothing in case of failure, so
I guess, I could have also did |
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 was surprised that the test suite for LocalBitcoinNode
wasn't automatically executed, or compiled, when I was running ./gradlew desktop:build
and desktop:run
.
Co-Authored-By: sqrrm <[email protected]>
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.
Tested it and it works as expected, nice.
The errors logged by NioClientManager would be good to get rid of since most users won't be running with a local node so they would have big errors on startup. Not that familiar with logging, but if you have a way to do it I would say it's good.
I think the unchecked usages of Optional
should be checked, perhaps use orElse()
, didn't comment on all uses as there are quite a few.
NIT: Prefer no empty line between comment and method.
|
||
// Here we only want to provide the user with a choice (in a popup) in case a local node is | ||
// detected, but badly configured. | ||
var detectedButMisconfigured = localBitcoinNode.isDetectedButMisconfigured().get(); |
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.
Since localBitcoinNode.isDetectedButMisconfigured()
is an optional it would be prudent to check that it's present here. Although it might be certain for now that it's never null the characteristics of an optional is that it should be able to handle the null state.
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 took your suggestion about getting rid of most of the Optional.get()s in other places, but in my opinion here a check would be superfluous, because it's easy to see that getting an empty Optional is impossible here with the current implementation of LocalBitcoinNode (.checkUsable()
on the previous line guarantees that). That said if it were to change, all the calls would have to be reconsidered anyway, right?
@@ -862,7 +879,7 @@ private void maybeShowSecurityRecommendation() { | |||
} | |||
|
|||
private void maybeShowLocalhostRunningInfo() { | |||
maybeTriggerDisplayHandler("bitcoinLocalhostNode", displayLocalhostHandler, localBitcoinNode.isDetected()); | |||
maybeTriggerDisplayHandler("bitcoinLocalhostNode", displayLocalhostHandler, localBitcoinNode.isUsable().get()); |
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.
Same here with the usage of Optional
without checking isPresent()
.
// TODO: what's the effect of NetworkParameters on handshake? | ||
// i.e. is it fine to just always use MainNetParams? | ||
var networkParameters = new MainNetParams(); | ||
|
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.
So much space in this method...
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.
Yes, please apply same formatting advice here as I mentioned above on lines 65–67. And apply it consistently to any other similar changes in this PR as well. The general rule in effect here is to keep things on one line up to the 120-char right margin.
var optionalVersionMessage = attemptHandshakeForVersionMessage(); | ||
handleHandshakeAttempt(optionalVersionMessage); | ||
// We know that the Optional/s will be populated by the end of the checks. | ||
return isUsable().get(); |
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.
Same with the checks for Optional.isPresent()
.
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.
NACK on formatting and style issues. There are more than just a couple minor nits here, stuff that's worth getting right with a view toward future PRs being correct from the start.
This is by no means a complete review. I just took a quick look to understand what's changed with the new LocalBitcoinNode
class and saw these superficial issues and commented on them. Unfortunately I don't have time to do more than that now.
private static NioClient createClient( | ||
Peer peer, int port, int connectionTimeout | ||
) throws IOException { |
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.
To follow convention, formatting here should be:
private static NioClient createClient(Peer peer, int port, int connectionTimeout) throws IOException {
// TODO: what's the effect of NetworkParameters on handshake? | ||
// i.e. is it fine to just always use MainNetParams? | ||
var networkParameters = new MainNetParams(); | ||
|
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.
Yes, please apply same formatting advice here as I mentioned above on lines 65–67. And apply it consistently to any other similar changes in this PR as well. The general rule in effect here is to keep things on one line up to the 120-char right margin.
* and | ||
* core/src/main/java/org/bitcoinj/core/NetworkParameters.java | ||
*/ | ||
|
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.
Nit: unnecessary newline here, and this need not be a (/**
) Javadoc comment as it's on a private method and will never be published. Wrap comments at 90-char right margin, thanks.
// The service bit is defined in BIP111. | ||
int NODE_BLOOM = 1 << 2; | ||
|
||
int BLOOM_FILTERS_BIP37_PROTOCOL_VERSION = 70000; |
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.
Local variables should be camelCase.
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.
Do you feel strongly about this? These are constants, in the semantic sense, that I encapsulated in a single method with the code that uses them, and formating them as such helps single them out. I didn't want to separate them out to top-level static variables, because they're part of a small port from upstream BitcoinJ and I wanted to keep it completely separate from the rest of the class.
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 opinion this kind of containment is good. Camel case local variables would not be appropriate, but making them local seems right considering the situation.
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.
utACK
I will test this before merging. @dmos62 if you feel like removing some of the extra empty lines in some of the methods meanwhile, please don't hesitate. I find the code harder to read with too much space as it breaks the flow compared with the other parts of the project.
// The service bit is defined in BIP111. | ||
int NODE_BLOOM = 1 << 2; | ||
|
||
int BLOOM_FILTERS_BIP37_PROTOCOL_VERSION = 70000; |
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 opinion this kind of containment is good. Camel case local variables would not be appropriate, but making them local seems right considering the situation.
@sqrrm could you point out examples of empty lines you would remove? I put some consideration into where I put empty lines. Usually to outline distinct steps in a method. Would appreciate feedback. |
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 added a comment on some of the blank lines I would remove. Personally I would probably make it even tighter though.
I understand the motivation of separating some of the concepts with blank lines, but it doesn't really help with readability since the methods themselves already contain the scope of activity.
// This initiates the handshake procedure, which, if successful, will complete | ||
// the peerVersionMessageFuture, or be cancelled, in case of failure. | ||
NioClient client = new NioClient(serverAddress, peer, connectionTimeout); | ||
|
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.
Blank line
// We must construct a BitcoinJ Context before using BitcoinJ. We don't keep a | ||
// reference, because it's automatically kept in a thread local storage. | ||
new Context(networkParameters); | ||
|
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.
Blank line
new Context(networkParameters); | ||
|
||
var ourVersionMessage = new VersionMessage(networkParameters, 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.
Blank line
var ourVersionMessage = new VersionMessage(networkParameters, 0); | ||
|
||
var localPeerAddress = new PeerAddress(InetAddress.getLocalHost(), port); | ||
|
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.
Blank line
var localPeerAddress = new PeerAddress(InetAddress.getLocalHost(), port); | ||
|
||
AbstractBlockChain blockchain = null; | ||
|
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.
Blank line
|
||
peer.close(); | ||
client.closeConnection(); | ||
|
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.
Blank line
|
||
private ListenableFuture<VersionMessage> getVersionMessage(Peer peer) { | ||
SettableFuture<VersionMessage> peerVersionMessageFuture = SettableFuture.create(); | ||
|
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.
Blank line
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.
Here rationale is to separate the creation of the future from the parts that use it. The next three parts of the method operate on this future.
* stacktrace). | ||
*/ | ||
var whenChecksNotFinished = false; | ||
|
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.
Blank line
var whenChecksNotFinished = false; | ||
|
||
var throwable = new Throwable("LocalBitcoinNode was queried before it was ready"); | ||
|
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.
Blank line
// Optional<Boolean> istead of boolean, an empty Optional signifying that the relevant | ||
// check has not yet been performed. | ||
var usableLocalNodePresent = localBitcoinNode.safeIsUsable(); | ||
|
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.
Blank line
@sqrrm thanks for the feedback. I appreciate it. In all your noted cases my motivation was to make visual some structure I saw in the methods. I commented under one of them detailing that a bit further. I would not feel comfortable removing those empty lines. We could write this off as stylistic differences. I'll say again that I do appreciate the feedback: I wasn't aware that this feature of my code was jarring to some. Further thinking on this, I would say that I'm minimally sensitive to vertical verbosity (or horizontal for that matter). It might have to do with how I use my vim when working. I jump around the code a lot and information density on-screen at any given time is not really a direct factor for me. |
I suggest disengaging from style discussions at this point. If the substantive changes in this PR are otherwise ACK-worthy, then merge it, and someone can issue a follow-up PR correcting the deviations. It’ll be faster that way. I’ll do it myself, even.
Alternatively, one of the maintainers could push those changes against the current PR branch, but it’s probably better in this situation to have a separate PR that documents the rework necessary.
@dmos, thanks for your contributions, but in a large established codebase like Bisq, a contributor’s goal should be to produce stylistically perfect patches, where perfect is defined by not causing reviewers to digress into style discussions or needing to clean up after you.
It takes some time to catch up with the nuances of style conventions in any large codebase and Bisq is far from perfectly consistent with its own conventions. As such, it’s expected that newcomers will make mistakes here and there; that’s not a problem. The main thing is to strive for patches that require as little overhead as possible for reviewers and maintainers, and style quibbles are overhead.
…> On Feb 26, 2020, at 8:10 PM, dmos62 ***@***.***> wrote:
@dmos62 commented on this pull request.
In core/src/main/java/bisq/core/btc/nodes/LocalBitcoinNode.java:
> + var mayInterruptWhileRunning = true;
+ peerVersionMessageFuture.cancel(mayInterruptWhileRunning);
@sqrrm the reason why I use these is to not have "anonymous" values in calls. Example: buildHouse(false) v. buildHouse(withRoof).
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
It looks like my previous message was sent too hastily. I sent it from my phone, where the last notifications I saw on this review thread were about style quibbles (explanatory variable declarations in particular). On returning to my laptop, I see that @dmos62 has followed up with commits that largely resolve the stylistic deviations. I am wondering, however, why |
I've just pushed a style polishing commit to my own fork at cbeams@b93ca2b. @dmos62, please consider pushing it to this PR branch. I am also working on an additional commit that makes several more substantive changes. I'll wrap it up and push it it to my branch in my (European) AM tomorrow (Thursday). Please let me know if you have any significant local changes that haven't been pushed, so I'm aware whether or not conflicts are likely. Thanks. |
@cbeams I don't have changes in the works. The test suite was removed because I don't have ideas about how to replace it. Before the detection mechanism was just checking if a socket can be opened; now a whole Bitcoin protocol handshake has to be performed. I don't mean to be a nuisance with the stylistic discussions. I guess I didn't consider the additional effort the differences created. I'll try and emulate the rest of the codebase. |
- Rename #willUse => #shouldBeUsed - Rename #willIgnore => #shouldBeIgnored - Reduce visibility of methods where appropriate - Edit Javadoc typos and use @link syntax where appropriate
This was originally added with the intention that the local Bitcoin node port could be customized, but in fact it never could be, because Guice configuration always hard-wired the value to the default port for the CurrentBaseNetwork's Parameters (eg. 8333 for BTC_MAINNET). This change removes the constant, removes any Guice wiring and injection and localizes the hard-coded assignment to the LocalBitcoinNode constructor to simplify and make things explicit. If it is desired to allow users to specify a custom port for their local Bitcoin node, a proper option shoud be added to Config. In the meantime, users may work around this by using `--btcNodes=localhost:4242` where 4242 is the custom port. Note however, that the pruning and bloom filter checks will not occur in this case as the provided node address will not being treated as a LocalBitcoinNode.
@dmos62, I've just submitted dmos62#1 with my changes, including the first polishing commit I mentioned above. Please merge and push that and the changes will show up here in this PR. Note that I have NOT actually tested the functionality of this larger PR on my local machine, so I'm deferring to @ripcurlx's outstanding review comment regarding his current inability to get the behavior to work as expected. |
Just tested the current state again on Regtest. Background information for my local Bitcoin Core is shown again when configured properly Warning is now shown when peerbloomfilter is not enabled When trying to set e.g. Following exceptions are logged in the command line by bitcoinJ which is expected I guess.
So ACK from my side now from a functionality point of view. Please apply the polishing commit by @cbeams and I'm happy to merge this PR. Thanks for your contribution @dmos62 ! |
Did we consider at any point in this process doing these same pruning and bloom filter checks not only against a local Bitcoin node, but also against (a) Bisq's provided federation of nodes and (b) any custom nodes provided by the user? In the case of (a), it's probably a waste to do these checks, since our own federation nodes should always be properly configured, but case (b) is more important, especially for users who are pointing their local Bisq node at their own remote Bitcoin full node. See the last paragraph of the commit comment at dmos62@57b7041 for more on this. In any case, I'm not suggesting that doing these checks against the federation or user-specified custom nodes should be done as part of this PR. I just want to see if it's been thought about at all. |
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.
NACK per #3982 (comment)
Atm we temp deactivated the option to connect to the public network because of the lack of a node capabilities check. I think after this PR is merged this would be the next step if we are able to apply this checks also to our public network and user-specified custom node option. |
Did you mean "to our federation" here? I don't see a lot of value in re-enabling the public network option at this point. So long as Bisq relies on SPV, users are at risk on the public network because of chain surveillance. I think keeping it just to our own federation or a user's custom node(s) is sufficient. Doing checks against (at least) user-provided custom nodes would make sense to me for the same reasons doing it against their local node makes sense. |
@@ -50,7 +50,6 @@ | |||
import java.io.File; | |||
|
|||
import static bisq.common.config.Config.*; | |||
import static bisq.core.btc.nodes.LocalBitcoinNode.LOCAL_BITCOIN_NODE_PORT; |
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 catch, thanks @dmos62.
I was talking about "public" public network. But yes, we can keep it deactivated for security reasons if on-one wants to explicitly use this feature because of specific reasons. |
Wow, @dmos62 that wording is really clear! More projects should be like this! |
Refactors LocalBitcoinNode and adds detection for local Bitcoin node's
configuration, namely, whether it is pruning and whether it has bloom
filter queries enabled.
The local node's configuration (and its presence) is retrieved by
performing a Bitcoin protocol handshake, which includes the local
Bitcoin node sending us its version message (VersionMessage in
BitcoinJ), which contains the information we're interested in.
Due to some quirky BitcoinJ logic, sometimes the handshake is
interrupted, even though we have received the local node's version
message. That contributes to the handshake handling in LocalBitcoinNode
being a bit complicated.
Refactoring consists of two principle changes: the public interface is
split into methods that trigger checks and methods that retrieve the
cached results. The methods that trigger checks have names starting
with "check", and methods that retrieve the cached results have names
that start with "is".
The other major refactor is the use of Optional instead of
boolean for storing and returning the results, an empty Optional
signifying that the relevant check was not yet performed. Switching to
Optionals has caused other code that queries LocalBitcoinNode to throw
an exception in case the query is made before the checks are. Before,
the results were instantiated to "false" and that would be returned
in case the query was made before the checks completed. This change has
revealed one occasion (Preferences class) where this happens.
This is shown in case a local Bitcoin node is found running, but misconfigured:
This is shown when setup with a local Bitcoin node is completed, to remind the user to have the node fully synced before using Bisq, and what the other requirements are:
I hope to receive some phrasing corrections and maybe some UI ideas? I'm not totally happy with the "information" popup above: it's a bit redundant.
Possible next steps:
I could have spent a bit more time and implemented these features too, but it's already two weeks that I'm doing this, so I'm just thrilled that I have something to PR. I can work on these later; I'll let you decide if these features are worthwhile.
Relevant issue: #3273