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

[WIP] Adds support for NetworkAPI to use a remote Bitcoin node #76

Merged
16 commits merged into from
Aug 21, 2019

Conversation

bjarnemagnussen
Copy link
Collaborator

@bjarnemagnussen bjarnemagnussen commented May 25, 2019

This PR adds a method allowing the NetworkAPI to connect to a remote Bitcoin node instead of relying on web APIs. It is marked as a work-in-progress to allow testing, code improvements and test cases to be added.

It has been implemented in a way to minimize changes to the codebase and to not break already existing tests. Any suggestions however on how to better connect the new class bit.network.services.RPCHost with the NetworkAPI is welcome! Right now inside NetworkAPI it simply overrides its web-API values using the RPCHost instead.

This implementation also allows to connect to two different Bitcoin nodes: one for mainnet and one for testnet; and keeps them separate.

Example code

import bit

# To connect to a remote Bitcoin node:
from bit.network import NetworkAPI
NetworkAPI.connect_to_node(user='user', password='password', host='localhost', port='18443', use_https=False, testnet=True)

# We could additionally choose to connect to some mainnet node:
NetworkAPI.connect_to_node(user='user', password='password', host='domain', port='8332', use_https=True, testnet=False)

# Do some bit stuff...
k_test = bit.PrivateKeyTestnet()
k_test.get_unspents()  # will use the testnet node for API
k_main = bit.PrivateKey()
k_main.get_unspents()  # will use the mainnet node for API
# ...

Remarks

Bitcoin Core is not a blockchain explorer and thus only keeps full track of addresses in its wallet! Therefore to use it as a remote node all addresses generated with Bit and that should be tracked must be added as (watch-only) addresses to Bitcoin Core as follows:

>>> bitcoin-cli importaddress "WATCH_ONLY_ADDRESS"

See importaddress for more details.

It may be useful to add a helper function that can add generated Bit addresses to the Bitcoin Core wallet.

Bitcoin Core RPC behaves particular with regard to coinbase transactions

Coinbase transactions directly received inside a Bitcoin Core wallet address will as expected be returned as a list of Unspent objects when using PrivateKey().get_unused(). However, they will not show up when using PrivateKey().get_transactions(). A fix would be to rewrite the RPCHost method get_transactions to use the RPC method listsinceblock, which returns all transactions associated to the Bitcoin Core wallet but hence also requires Bit to filter them by addresses. As this is an edge case and wasteful for big wallets it has been ignored (for now).
See the discussion here.

TODOs

@ghost
Copy link

ghost commented May 26, 2019

Wow, this is awesome! Will need to look closer later.

Thank you!

@ofek
Copy link
Owner

ofek commented May 27, 2019

This is so cool!!! I'll try to review sometime this week. Can we add docs?

@bjarnemagnussen
Copy link
Collaborator Author

I have added "docs" to the TODO list :)

Copy link
Owner

@ofek ofek left a comment

Choose a reason for hiding this comment

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

This LGTM! Do you think it would be worth it/possible to test this in CI?

@bjarnemagnussen
Copy link
Collaborator Author

Yes, I also think we should be able to test it. I will look into this :)

@ghost
Copy link

ghost commented Jun 19, 2019

Finally looked over the code. I agree that CI testing would be good if possible, but I have to say that this looks great as it is. Very nice work!

@ofek
Copy link
Owner

ofek commented Jul 8, 2019

Can we merge?

@bjarnemagnussen
Copy link
Collaborator Author

I have been a bit busy and therefore didn't have the time to add tests yet. But I will look into this in the next two weeks. After that I think we should be ready to merge!

Fixes precision error due to Bitcoin Core node returning the balance in BTC as float instead of in satoshis as integer.
@bjarnemagnussen
Copy link
Collaborator Author

I have made two testing classes for the RPCHost and RPCMethod, but I am not sure if this is the most optimal way to do it.

To make the tests I have created two mock classes:

  • MockRPCMethod, which returns test data when invoked, and
  • MockRPCHost, which inherits from RPCHost but calls MockRPCMethod instead.

The tests for RPCHost are found inside the class TestRPCHost. It both checks if the initialization is as expected and that the calls to the different functions (which values are returned by MockRPCMethod) are processed as expected.

The tests for RPCMethod are found inside the class TestRPCMethod. It makes use of a module requests_mock, that is used to check that the RPCMethod class does request correctly formatted data from a node. Using requests_mock was for me the only way to easily make those tests possible, but if anyone has a better idea on how to test the requests from RPCMethod let me know!

Due to the import of requests_mock Travis CI is broken right now. If we are going to use this module it should be added to Travis.

@ofek
Copy link
Owner

ofek commented Jul 14, 2019

Great! Sure, add it here

bit/tox.ini

Line 8 in 0b0e553

deps =

response = self._host._session.post(
self._host._url, headers=self._host._headers, data=payload
)
except requests.exceptions.ConnectionError:
Copy link
Owner

Choose a reason for hiding this comment

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

Question: should we catch more exceptions to avoid it printing/logging the url w/ password inside?

Copy link

Choose a reason for hiding this comment

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

Interesting point, but I think logs are generally not regarded as public anyway. I don't see that being a blocker here. Does requests already block passwords, perhaps?

Copy link
Owner

Choose a reason for hiding this comment

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

Requests does not, but no need, you're right

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I would be open for suggestions to improve this. I am just not sure how.

Copy link

@ghost ghost left a comment

Choose a reason for hiding this comment

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

Looks pretty good overall, just a few small comments.

def get_balance(self, address):
getcontext().prec = 9
b = Decimal(self.getreceivedbyaddress(address, 0))
return int(float(str(b * 100000000)))
Copy link

Choose a reason for hiding this comment

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

Don't we already have constants in place for this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, you are right. They are inside network/rates.py. I will change it to make use of them.

def broadcast_tx(self, tx_hex):
try:
_ = self.sendrawtransaction(tx_hex)
except BitcoinNodeException:
Copy link

Choose a reason for hiding this comment

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

Would it be worth at least logging the exeption message as a warning? I'm not certain but curious what your thoughts are.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Makes sense. I forgot about it but it is important to find out why a transaction eventually fails to be broadcast. Will add a logging warning!

response = self._host._session.post(
self._host._url, headers=self._host._headers, data=payload
)
except requests.exceptions.ConnectionError:
Copy link

Choose a reason for hiding this comment

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

Interesting point, but I think logs are generally not regarded as public anyway. I don't see that being a blocker here. Does requests already block passwords, perhaps?

if args[2][0] in (MAIN_ADDRESS_UNUSED, TEST_ADDRESS_UNUSED):
return []
return [{
"txid": "381f1605dd927151fbfac2e88608464414fa5b01bd6298cd1e2d9d991907aa9e",
Copy link

Choose a reason for hiding this comment

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

These are fine, but like with my earlier comment about constants, I think we have samples with a lot of these.

I guess if they're used only once there's no reason to add them as samples.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree! However, transaction constants are found inside test_transaction.py, not samples.py. And I think we should probably generally clean up the sample constants. Over time we may have even introduced redundant constants, etc. Maybe this is something that should be done in a future PR, which cleans up the sample constants?

Copy link

Choose a reason for hiding this comment

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

Sure, that's fine.

@bjarnemagnussen
Copy link
Collaborator Author

I have added a few more tests, of which especially under TestNetworkAPI the tests test_connect_to_node_main and test_connect_to_node_test are a little bit involved. Basically they test that after calling connect_to_node only the appropriate "constants" inside NetworkAPI are overriden with a RPCHost of correct values. For future compatibility the tests handles connection to multiple RPCHosts.

Copy link

@ghost ghost left a comment

Choose a reason for hiding this comment

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

Looks great to me. Nice work!

Copy link
Owner

@ofek ofek left a comment

Choose a reason for hiding this comment

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

Great job!!!

@bjarnemagnussen
Copy link
Collaborator Author

Thanks! I have just added one little commit. Since we are actually only using the equality method of RPCHost for the tests in TestNetworkAPI, I have moved the method to become a helper function both_rpchosts_equal inside test_services.py.

Removes the equal method from `RPCHost` and adds it as a helper function to `test_services.py`. Also improves a little on the readability of the tests `test_connect_to_node_main` and `test_connect_to_node_test`.
@ghost
Copy link

ghost commented Jul 17, 2019

Looks great to me! Merge whenever you feel ready.

@bjarnemagnussen
Copy link
Collaborator Author

The only last thing I think is missing is a helper function that adds an address to the local node. I will try to add this very soon.

@ofek
Copy link
Owner

ofek commented Aug 16, 2019

Hello again 👋

Can be used to e.g. call `importaddress` on the Bitcoin Core node, etc.
@bjarnemagnussen
Copy link
Collaborator Author

Hey again. I hope everyone had a nice summer holiday.

I have actually come to think that the best way to allow adding addresses to the node's wallet is to simply expose direct access the node's RPCs? That is a simple step and I have added documentation of it. Let me know what you think about it this way?

@bjarnemagnussen
Copy link
Collaborator Author

Uhhh... Looks like Travis CI is breaking because one of our used addresses MAIN_ADDRESS_USED1 that should have >0 balance belongs to the Bitstamp hacked and was emptied 2 weeks ago (https://www.blockchain.com/btc/address/1L2JsXHPMYuAa9ugvHGLwkdstCPUDemNCf)?

@bjarnemagnussen
Copy link
Collaborator Author

We should probably change the MAIN_ADDRESS_USED1 address to a "proof-of-burn" address, e.g. 1CounterpartyXXXXXXXXXXXXXXXUWLpVr. In that regard I am not sure why there are two addresses MAIN_ADDRESS_USED1 and MAIN_ADDRESS_USED2 (and the same for testnet)? However this is something we should fix in a separate PR.

Copy link
Owner

@ofek ofek left a comment

Choose a reason for hiding this comment

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

Excellent! Just left a few fix suggestions for docs formatting

docs/source/guide/remotenode.rst Outdated Show resolved Hide resolved
docs/source/guide/remotenode.rst Outdated Show resolved Hide resolved
docs/source/guide/remotenode.rst Outdated Show resolved Hide resolved
docs/source/guide/remotenode.rst Outdated Show resolved Hide resolved
docs/source/guide/remotenode.rst Outdated Show resolved Hide resolved
docs/source/guide/remotenode.rst Outdated Show resolved Hide resolved
docs/source/guide/remotenode.rst Outdated Show resolved Hide resolved
docs/source/guide/remotenode.rst Outdated Show resolved Hide resolved
docs/source/guide/remotenode.rst Outdated Show resolved Hide resolved
docs/source/guide/remotenode.rst Outdated Show resolved Hide resolved
Co-Authored-By: Ofek Lev <ofekmeister@gmail.com>
@bjarnemagnussen
Copy link
Collaborator Author

Just a comment to push notification. I have committed the suggested docs changes!

Copy link
Owner

@ofek ofek left a comment

Choose a reason for hiding this comment

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

Merge away!!!!!

@ghost
Copy link

ghost commented Aug 21, 2019

Nice work! I'm going to go ahead and merge this.

@ghost ghost merged commit 991d7f1 into ofek:master Aug 21, 2019
@bjarnemagnussen bjarnemagnussen deleted the local-node-support branch September 4, 2019 13:32
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants