nucypher/tests/unit/test_web3_clients.py

414 lines
14 KiB
Python
Raw Normal View History

"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
2019-08-07 20:31:38 +00:00
import datetime
from unittest.mock import Mock, PropertyMock
import maya
2019-08-07 20:31:38 +00:00
import pytest
from web3 import HTTPProvider, IPCProvider, WebsocketProvider
from web3.types import RPCResponse, RPCError
2019-08-07 20:31:38 +00:00
from nucypher.blockchain.eth.clients import (EthereumClient, GanacheClient, GethClient, InfuraClient, PUBLIC_CHAINS,
ParityClient)
2019-06-18 05:38:01 +00:00
from nucypher.blockchain.eth.interfaces import BlockchainInterface
from nucypher.blockchain.eth.providers import make_rpc_request_with_retry, _is_alchemy_retry_response
DEFAULT_GAS_PRICE = 42
GAS_PRICE_FROM_STRATEGY = 1234
#
# Mock Providers
#
2019-05-29 13:42:48 +00:00
class MockGethProvider:
endpoint_uri = 'file:///ipc.geth'
2019-05-29 13:42:48 +00:00
clientVersion = 'Geth/v1.4.11-stable-fed692f6/darwin/go1.7'
class MockParityProvider:
endpoint_uri = 'file:///ipc.parity'
2019-05-29 13:42:48 +00:00
clientVersion = 'Parity-Ethereum/v2.5.1-beta-e0141f8-20190510/x86_64-linux-gnu/rustc1.34.1'
class MockGanacheProvider:
endpoint_uri = 'http://ganache:8445'
2019-05-29 13:42:48 +00:00
clientVersion = 'EthereumJS TestRPC/v2.1.5/ethereum-js'
class MockInfuraProvider:
endpoint_uri = 'wss://:@goerli.infura.io/ws/v3/1234567890987654321abcdef'
clientVersion = 'Geth/v1.8.23-omnibus-2ad89aaa/linux-amd64/go1.11.1'
class MockWebSocketProvider:
endpoint_uri = 'ws://127.0.0.1:8546'
clientVersion = 'Geth/v1.8.23-omnibus-2ad89aaa/linux-amd64/go1.11.1'
2019-08-07 20:31:38 +00:00
class SyncedMockW3Eth:
# Support older and newer versions of web3 py in-test
version = 5
chainId = hex(5)
2019-08-07 20:31:38 +00:00
blockNumber = 5
def getBlock(self, blockNumber):
return {
'timestamp': datetime.datetime.timestamp(datetime.datetime.now() - datetime.timedelta(seconds=25))
}
class SyncingMockW3Eth(SyncedMockW3Eth):
_sync_test_limit = 10
2019-08-07 20:31:38 +00:00
def __init__(self, *args, **kwargs):
self._syncing_counter = 0
super().__init__(*args, **kwargs)
@property
def syncing(self):
if self._syncing_counter < self._sync_test_limit:
self._syncing_counter += 1
return {
'currentBlock': self._syncing_counter,
'highestBlock': self._sync_test_limit,
}
return False
def getBlock(self, blockNumber):
return {
'timestamp': datetime.datetime.timestamp(datetime.datetime.now() - datetime.timedelta(seconds=500))
}
class MockedW3GethWithPeers:
@property
def admin(self):
class GethAdmin:
def peers(self):
return [1, 2, 3]
return GethAdmin()
class MockedW3GethWithNoPeers:
@property
def admin(self):
class GethAdmin:
def peers(self):
return []
return GethAdmin()
2019-05-29 13:42:48 +00:00
#
# Mock Web3
#
2019-08-07 20:31:38 +00:00
class SyncedMockWeb3:
2019-05-29 13:42:48 +00:00
2019-08-07 20:31:38 +00:00
net = SyncedMockW3Eth()
eth = SyncedMockW3Eth()
geth = MockedW3GethWithPeers()
2019-05-29 13:42:48 +00:00
def __init__(self, provider):
self.provider = provider
@property
def clientVersion(self):
return self.provider.clientVersion
@property
def isConnected(self):
return lambda: True
2019-05-29 13:42:48 +00:00
2019-08-07 20:31:38 +00:00
class SyncingMockWeb3(SyncedMockWeb3):
net = SyncingMockW3Eth()
eth = SyncingMockW3Eth()
class SyncingMockWeb3NoPeers(SyncingMockWeb3):
2019-08-07 20:31:38 +00:00
geth = MockedW3GethWithNoPeers()
# Mock Blockchain
#
2019-06-18 05:38:01 +00:00
class BlockchainInterfaceTestBase(BlockchainInterface):
2019-05-29 13:42:48 +00:00
2019-08-07 20:31:38 +00:00
Web3 = SyncedMockWeb3
2019-05-29 13:42:48 +00:00
def _configure_registry(self, *args, **kwargs):
pass
def _setup_solidity(self, *args, **kwargs):
pass
def attach_middleware(self):
pass
2019-05-29 13:42:48 +00:00
class ProviderTypeTestClient(BlockchainInterfaceTestBase):
def __init__(self,
expected_provider_class,
actual_provider_to_attach,
*args,
**kwargs):
2019-08-23 18:01:01 +00:00
super().__init__(*args, **kwargs)
self.expected_provider_class = expected_provider_class
self.test_provider_to_attach = actual_provider_to_attach
def _attach_provider(self, *args, **kwargs) -> None:
super()._attach_provider(*args, **kwargs)
# check type
assert isinstance(self.provider, self.expected_provider_class)
super()._attach_provider(provider=self.test_provider_to_attach)
class InfuraTestClient(BlockchainInterfaceTestBase):
def _attach_provider(self, *args, **kwargs) -> None:
super()._attach_provider(provider=MockInfuraProvider())
2019-06-18 05:38:01 +00:00
class GethClientTestBlockchain(BlockchainInterfaceTestBase):
2019-05-29 13:42:48 +00:00
def _attach_provider(self, *args, **kwargs) -> None:
super()._attach_provider(provider=MockGethProvider())
2019-05-29 13:42:48 +00:00
@property
def is_local(self):
return int(self.w3.net.version) not in PUBLIC_CHAINS
2019-05-29 13:42:48 +00:00
2019-06-18 05:38:01 +00:00
class ParityClientTestInterface(BlockchainInterfaceTestBase):
2019-05-29 13:42:48 +00:00
def _attach_provider(self, *args, **kwargs) -> None:
super()._attach_provider(provider=MockParityProvider())
2019-05-29 13:42:48 +00:00
2019-06-18 05:38:01 +00:00
class GanacheClientTestInterface(BlockchainInterfaceTestBase):
2019-05-29 13:42:48 +00:00
def _attach_provider(self, *args, **kwargs) -> None:
super()._attach_provider(provider=MockGanacheProvider())
2019-05-29 13:42:48 +00:00
def test_client_no_provider():
with pytest.raises(BlockchainInterface.NoProvider) as e:
interface = BlockchainInterfaceTestBase()
interface.connect()
2019-05-29 13:42:48 +00:00
def test_geth_web3_client():
2019-06-21 18:07:06 +00:00
interface = GethClientTestBlockchain(provider_uri='file:///ipc.geth')
interface.connect()
2019-06-21 18:07:06 +00:00
2019-05-29 13:42:48 +00:00
assert isinstance(interface.client, GethClient)
assert interface.client.node_technology == 'Geth'
assert interface.client.node_version == 'v1.4.11-stable-fed692f6'
assert interface.client.platform == 'darwin'
assert interface.client.backend == 'go1.7'
assert interface.client.is_local is False
assert interface.client.chain_id == 5 # Hardcoded above
def test_autodetect_provider_type_file(tempfile_path):
interface = ProviderTypeTestClient(provider_uri=tempfile_path, # existing file for test
expected_provider_class=IPCProvider,
actual_provider_to_attach=MockGethProvider())
interface.connect()
assert isinstance(interface.client, GethClient)
def test_autodetect_provider_type_file_none_existent():
with pytest.raises(BlockchainInterface.UnsupportedProvider) as e:
interface = BlockchainInterfaceTestBase(provider_uri='/none_existent.ipc.geth')
interface.connect()
def test_detect_provider_type_file():
interface = ProviderTypeTestClient(provider_uri='file:///ipc.geth',
expected_provider_class=IPCProvider,
actual_provider_to_attach=MockGethProvider())
interface.connect()
assert isinstance(interface.client, GethClient)
def test_detect_provider_type_ipc():
interface = ProviderTypeTestClient(provider_uri='ipc:///ipc.geth',
expected_provider_class=IPCProvider,
actual_provider_to_attach=MockGethProvider())
interface.connect()
assert isinstance(interface.client, GethClient)
def test_detect_provider_type_http():
interface = ProviderTypeTestClient(provider_uri='http://ganache:8445',
expected_provider_class=HTTPProvider,
actual_provider_to_attach=MockGanacheProvider())
interface.connect()
assert isinstance(interface.client, GanacheClient)
def test_detect_provider_type_https():
interface = ProviderTypeTestClient(provider_uri='https://ganache:8445',
expected_provider_class=HTTPProvider,
actual_provider_to_attach=MockGanacheProvider())
interface.connect()
assert isinstance(interface.client, GanacheClient)
def test_detect_provider_type_ws():
interface = ProviderTypeTestClient(provider_uri='ws://127.0.0.1:8546',
expected_provider_class=WebsocketProvider,
actual_provider_to_attach=MockWebSocketProvider())
interface.connect()
assert isinstance(interface.client, GethClient)
def test_infura_web3_client():
interface = InfuraTestClient(provider_uri='infura://1234567890987654321abcdef')
interface.connect()
assert isinstance(interface.client, InfuraClient)
assert interface.client.node_technology == 'Geth'
assert interface.client.node_version == 'v1.8.23-omnibus-2ad89aaa'
assert interface.client.platform == 'linux-amd64'
assert interface.client.backend == 'go1.11.1'
assert interface.client.is_local is False
assert interface.client.chain_id == 5
assert interface.client.unlock_account('address', 'password') # Returns True on success
2019-05-29 13:42:48 +00:00
def test_parity_web3_client():
2019-06-21 18:07:06 +00:00
interface = ParityClientTestInterface(provider_uri='file:///ipc.parity')
interface.connect()
2019-06-21 18:07:06 +00:00
2019-05-29 13:42:48 +00:00
assert isinstance(interface.client, ParityClient)
assert interface.client.node_technology == 'Parity-Ethereum'
assert interface.client.node_version == 'v2.5.1-beta-e0141f8-20190510'
assert interface.client.platform == 'x86_64-linux-gnu'
assert interface.client.backend == 'rustc1.34.1'
2019-05-29 13:42:48 +00:00
def test_ganache_web3_client():
2019-06-21 18:07:06 +00:00
interface = GanacheClientTestInterface(provider_uri='http://ganache:8445')
interface.connect()
2019-06-21 18:07:06 +00:00
2019-05-29 13:42:48 +00:00
assert isinstance(interface.client, GanacheClient)
assert interface.client.node_technology == 'EthereumJS TestRPC'
assert interface.client.node_version == 'v2.1.5'
assert interface.client.platform is None
assert interface.client.backend == 'ethereum-js'
assert interface.client.is_local
def test_gas_prices(mocker, mock_ethereum_client):
web3_mock = mock_ethereum_client.w3
web3_mock.eth.generateGasPrice = mocker.Mock(side_effect=[None, GAS_PRICE_FROM_STRATEGY])
type(web3_mock.eth).gasPrice = PropertyMock(return_value=DEFAULT_GAS_PRICE) # See docs of PropertyMock
assert mock_ethereum_client.gas_price == DEFAULT_GAS_PRICE
assert mock_ethereum_client.gas_price_for_transaction("there's no gas strategy") == DEFAULT_GAS_PRICE
assert mock_ethereum_client.gas_price_for_transaction("2nd time is the charm") == GAS_PRICE_FROM_STRATEGY
def test_alchemy_rpc_request_with_retry():
retries = 4
# Retry Case - RPCResponse fails due to limits, and retry required
retry_responses = [
RPCResponse(error=RPCError(code=-32000,
message='Your app has exceeded its compute units per second capacity. If you have '
'retries enabled, you can safely ignore this message. If not, '
'check out https://docs.alchemyapi.io/guides/rate-limits')),
RPCResponse(error=RPCError(code=429, message='Too many concurrent requests')),
RPCResponse(error='Your app has exceeded its compute units per second capacity. If you have retries enabled, '
'you can safely ignore this message. If not, '
'check out https://docs.alchemyapi.io/guides/rate-limits')
]
for test_response in retry_responses:
provider = Mock()
provider.make_request.return_value = test_response
retry_response = make_rpc_request_with_retry(provider,
is_retry_response=_is_alchemy_retry_response,
logger=None,
num_retries=retries,
exponential_backoff=False) # disable exponential backoff
assert retry_response == test_response
assert provider.make_request.call_count == (retries + 1) # one call, and then the number of retries
def test_alchemy_rpc_request_success_with_no_retry():
# Success Case - retry not needed
provider = Mock()
successful_response = RPCResponse(id=0, result='0xa1c054')
provider.make_request.return_value = successful_response
retry_response = make_rpc_request_with_retry(provider,
is_retry_response=_is_alchemy_retry_response,
logger=None,
num_retries=10,
exponential_backoff=False) # disable exponential backoff
assert retry_response == successful_response
assert provider.make_request.call_count == 1 # first request was successful, no need for retries
# TODO - since this test does exponential backoff it takes >= 2^1 = 2s, should we only run on circleci?
def test_alchemy_rpc_request_with_retry_exponential_backoff():
retries = 1
provider = Mock()
# Retry Case - RPCResponse fails due to limits, and retry required
test_response = RPCResponse(error=RPCError(code=429, message='Too many concurrent requests'))
provider.make_request.return_value = test_response
start = maya.now()
retry_response = make_rpc_request_with_retry(provider,
is_retry_response=_is_alchemy_retry_response,
logger=None,
num_retries=retries,
exponential_backoff=True) # enable exponential backoff
end = maya.now()
assert retry_response == test_response
assert provider.make_request.call_count == (retries + 1) # one call, and then the number of retries
# check exponential backoff
delta = end - start
assert delta.total_seconds() >= 2**retries