From 8cbb84f44a3a96787afca385677dc37097de4990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20M=C3=BCller?= Date: Thu, 18 Oct 2018 11:49:18 +0200 Subject: [PATCH 1/7] Prepare fuzz testing (property testing) --- raiden/tests/unit/fuzz/conftest.py | 7 +++++++ raiden/tests/unit/fuzz/test_initiator_state_changes.py | 0 requirements-dev.txt | 2 ++ 3 files changed, 9 insertions(+) create mode 100644 raiden/tests/unit/fuzz/conftest.py create mode 100644 raiden/tests/unit/fuzz/test_initiator_state_changes.py diff --git a/raiden/tests/unit/fuzz/conftest.py b/raiden/tests/unit/fuzz/conftest.py new file mode 100644 index 0000000000..f0ae321380 --- /dev/null +++ b/raiden/tests/unit/fuzz/conftest.py @@ -0,0 +1,7 @@ +import pytest + + +@pytest.fixture(autouse=True) +def override_capture_setting_for_hypothesis_tests(request): + """override the general setting to see failed paths generated by Hypothesis """ + request.config.option.showcapture = 'all' diff --git a/raiden/tests/unit/fuzz/test_initiator_state_changes.py b/raiden/tests/unit/fuzz/test_initiator_state_changes.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/requirements-dev.txt b/requirements-dev.txt index 3d736695b1..cbd3f8c160 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,6 +10,8 @@ pytest-timeout==1.2.1 grequests==0.3.0 pexpect==4.6.0 +hypothesis==3.80.0 + eth-tester==0.1.0b32 # Debugging From b630f887cc00f23207d3a9b03cb2817dda180c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20M=C3=BCller?= Date: Fri, 26 Oct 2018 19:02:54 +0200 Subject: [PATCH 2/7] Add sample fuzz test for initiator role state changes --- .../tests/unit/fuzz/test_initiator_state.py | 135 ++++++++++++++++++ .../unit/fuzz/test_initiator_state_changes.py | 0 2 files changed, 135 insertions(+) create mode 100644 raiden/tests/unit/fuzz/test_initiator_state.py delete mode 100644 raiden/tests/unit/fuzz/test_initiator_state_changes.py diff --git a/raiden/tests/unit/fuzz/test_initiator_state.py b/raiden/tests/unit/fuzz/test_initiator_state.py new file mode 100644 index 0000000000..d2086eff79 --- /dev/null +++ b/raiden/tests/unit/fuzz/test_initiator_state.py @@ -0,0 +1,135 @@ +from hypothesis import assume +from hypothesis.stateful import Bundle, RuleBasedStateMachine, initialize, rule +from hypothesis.strategies import binary, composite, integers, random_module, randoms + +from raiden.tests.utils import factories +from raiden.transfer import channel, node +from raiden.transfer.mediated_transfer.events import SendLockedTransfer, SendSecretReveal +from raiden.transfer.mediated_transfer.state_change import ( + ActionInitInitiator, + ReceiveSecretRequest, + TransferDescriptionWithSecretState, +) +from raiden.transfer.state import ChainState, PaymentNetworkState, TokenNetworkState +from raiden.transfer.state_change import ContractReceiveChannelNew +from raiden.utils import sha3 + + +@composite +def secret(draw): + return draw(binary(min_size=32, max_size=32)) + + +def event_types_match(events, types): + return ( + len(events) == len(types) and + all(isinstance(event, type) for (event, type) in zip(events, types)) + ) + + +class InitiatorState(RuleBasedStateMachine): + + @initialize(block_number=integers(min_value=1), random=randoms(), random_seed=random_module()) + def initialize(self, block_number, random, random_seed): + self.random_seed = random_seed + + self.block_number = block_number + self.random = random + self.private_key, self.address = factories.make_privkey_address() + + self.chain_state = ChainState( + self.random, + self.block_number, + self.address, + factories.UNIT_CHAIN_ID, + ) + + self.token_network_id = factories.make_address() + self.token_id = factories.make_address() + self.token_network_state = TokenNetworkState(self.token_network_id, self.token_id) + + self.payment_network_id = factories.make_payment_network_identifier() + self.payment_network_state = PaymentNetworkState( + self.payment_network_id, + [self.token_network_state], + ) + + self.chain_state.identifiers_to_paymentnetworks[ + self.payment_network_id + ] = self.payment_network_state + + self.channel = factories.make_channel( + our_balance=1000, + token_network_identifier=self.token_network_id, + ) + + channel_new_state_change = ContractReceiveChannelNew( + factories.make_transaction_hash(), + self.token_network_id, + self.channel, + self.block_number, + ) + + node.state_transition(self.chain_state, channel_new_state_change) + + new_transfers = Bundle('new_transfers') + pending_transfers = Bundle('pending_transfers') + + @rule( + target=new_transfers, + payment_id=integers(min_value=1), + amount=integers(min_value=1, max_value=100), + secret=secret(), + ) + def populate_transfer_descriptions(self, payment_id, amount, secret): + return TransferDescriptionWithSecretState( + self.payment_network_id, + payment_id, + amount, + self.token_network_id, + self.address, + self.channel.partner_state.address, # target + secret, + ) + + def _secret_in_use(self, secret): + return sha3(secret) in self.chain_state.payment_mapping.secrethashes_to_task + + def _available_amount(self): + deposit = self.channel.our_total_deposit + locked = channel.get_amount_locked(self.channel.our_state) + return deposit - locked + + def _action_init_initiator(self, transfer: TransferDescriptionWithSecretState): + return ActionInitInitiator( + transfer, + [factories.route_from_channel(self.channel)], + ) + + def _receive_secret_request(self, transfer: TransferDescriptionWithSecretState): + secrethash = sha3(transfer.secret) + return ReceiveSecretRequest( + transfer.payment_identifier, + transfer.amount, + self.block_number + 10, # todo + secrethash, + transfer.target, + ) + + @rule(target=pending_transfers, transfer=new_transfers) + def valid_init_transfer(self, transfer): + assume(not self._secret_in_use(transfer.secret)) + assume(transfer.amount <= self._available_amount()) + action = self._action_init_initiator(transfer) + result = node.state_transition(self.chain_state, action) + assert event_types_match(result.events, [SendLockedTransfer]) + return transfer + + @rule(transfer=pending_transfers) + def valid_secret_request(self, transfer): + action = self._receive_secret_request(transfer) + result = node.state_transition(self.chain_state, action) + assert event_types_match(result.events, [SendSecretReveal]) + + +TestInitiator = InitiatorState.TestCase diff --git a/raiden/tests/unit/fuzz/test_initiator_state_changes.py b/raiden/tests/unit/fuzz/test_initiator_state_changes.py deleted file mode 100644 index e69de29bb2..0000000000 From 3a4acde6f2a8029902eed0229636b77769df1d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20M=C3=BCller?= Date: Sun, 28 Oct 2018 22:53:13 +0100 Subject: [PATCH 3/7] Extend initiator fuzz test, add failing paths --- .../tests/unit/fuzz/test_initiator_state.py | 186 +++++++++++++----- 1 file changed, 142 insertions(+), 44 deletions(-) diff --git a/raiden/tests/unit/fuzz/test_initiator_state.py b/raiden/tests/unit/fuzz/test_initiator_state.py index d2086eff79..0efd653979 100644 --- a/raiden/tests/unit/fuzz/test_initiator_state.py +++ b/raiden/tests/unit/fuzz/test_initiator_state.py @@ -1,9 +1,15 @@ +from copy import deepcopy +from random import Random + +import pytest from hypothesis import assume from hypothesis.stateful import Bundle, RuleBasedStateMachine, initialize, rule from hypothesis.strategies import binary, composite, integers, random_module, randoms +from raiden.constants import GENESIS_BLOCK_NUMBER from raiden.tests.utils import factories from raiden.transfer import channel, node +from raiden.transfer.events import EventPaymentSentFailed from raiden.transfer.mediated_transfer.events import SendLockedTransfer, SendSecretReveal from raiden.transfer.mediated_transfer.state_change import ( ActionInitInitiator, @@ -20,16 +26,17 @@ def secret(draw): return draw(binary(min_size=32, max_size=32)) -def event_types_match(events, types): - return ( - len(events) == len(types) and - all(isinstance(event, type) for (event, type) in zip(events, types)) - ) +def events_include(events, type, number=1): + return len([event for event in events if isinstance(event, type)]) == number -class InitiatorState(RuleBasedStateMachine): +class ChainStateStateMachine(RuleBasedStateMachine): - @initialize(block_number=integers(min_value=1), random=randoms(), random_seed=random_module()) + @initialize( + block_number=integers(min_value=GENESIS_BLOCK_NUMBER), + random=randoms(), + random_seed=random_module(), + ) def initialize(self, block_number, random, random_seed): self.random_seed = random_seed @@ -72,33 +79,16 @@ def initialize(self, block_number, random, random_seed): node.state_transition(self.chain_state, channel_new_state_change) - new_transfers = Bundle('new_transfers') - pending_transfers = Bundle('pending_transfers') - @rule( - target=new_transfers, - payment_id=integers(min_value=1), - amount=integers(min_value=1, max_value=100), - secret=secret(), - ) - def populate_transfer_descriptions(self, payment_id, amount, secret): - return TransferDescriptionWithSecretState( - self.payment_network_id, - payment_id, - amount, - self.token_network_id, - self.address, - self.channel.partner_state.address, # target - secret, - ) +class InitiatorState(ChainStateStateMachine): - def _secret_in_use(self, secret): - return sha3(secret) in self.chain_state.payment_mapping.secrethashes_to_task + def __init__(self): + super().__init__() + self.failed_secret_requests = set() + self.initiated = set() - def _available_amount(self): - deposit = self.channel.our_total_deposit - locked = channel.get_amount_locked(self.channel.our_state) - return deposit - locked + self.failing_path_2 = False + self.failing_path_4 = False def _action_init_initiator(self, transfer: TransferDescriptionWithSecretState): return ActionInitInitiator( @@ -109,27 +99,135 @@ def _action_init_initiator(self, transfer: TransferDescriptionWithSecretState): def _receive_secret_request(self, transfer: TransferDescriptionWithSecretState): secrethash = sha3(transfer.secret) return ReceiveSecretRequest( - transfer.payment_identifier, - transfer.amount, - self.block_number + 10, # todo - secrethash, - transfer.target, + payment_identifier=transfer.payment_identifier, + amount=transfer.amount, + expiration=self.block_number + 10, # todo + secrethash=secrethash, + sender=transfer.target, + ) + + transfers = Bundle('transfers') + init_initiators = Bundle('init_initiators') + invalid_authentic_secret_requests = Bundle('invalid_authentic_secret_requests') + unauthentic_secret_requests = Bundle('unauthentic_secret_requests') + + @rule( + target=transfers, + payment_id=integers(min_value=1), + amount=integers(min_value=1, max_value=100), + secret=secret(), + ) + def populate_transfer_descriptions(self, payment_id, amount, secret): + return TransferDescriptionWithSecretState( + payment_network_identifier=self.payment_network_id, + payment_identifier=payment_id, + amount=amount, + token_network_identifier=self.token_network_id, + initiator=self.address, + target=self.channel.partner_state.address, + secret=secret, ) - @rule(target=pending_transfers, transfer=new_transfers) - def valid_init_transfer(self, transfer): + def _secret_in_use(self, secret): + return sha3(secret) in self.chain_state.payment_mapping.secrethashes_to_task + + def _available_amount(self): + return channel.get_distributable(self.channel.our_state, self.channel.partner_state) + + @rule(target=init_initiators, transfer=transfers) + def valid_init_initiator(self, transfer): + if not self.failing_path_2: + assume(transfer.payment_identifier not in self.initiated) assume(not self._secret_in_use(transfer.secret)) assume(transfer.amount <= self._available_amount()) action = self._action_init_initiator(transfer) result = node.state_transition(self.chain_state, action) - assert event_types_match(result.events, [SendLockedTransfer]) - return transfer + assert events_include(result.events, SendLockedTransfer) + self.initiated.add(transfer.payment_identifier) + return action + + @rule(previous_action=init_initiators) + def valid_secret_request(self, previous_action): + if not self.failing_path_4: + assume(previous_action.transfer.payment_identifier not in self.failed_secret_requests) + action = self._receive_secret_request(previous_action.transfer) + result = node.state_transition(self.chain_state, action) + assert events_include(result.events, SendSecretReveal) + + @rule( + target=invalid_authentic_secret_requests, + previous_action=init_initiators, + amount=integers(), + ) + def wrong_amount_secret_request(self, previous_action, amount): + assume(amount != previous_action.transfer.amount) + transfer = deepcopy(previous_action.transfer) + transfer.amount = amount + return self._receive_secret_request(transfer) + + @rule(action=invalid_authentic_secret_requests) + def invalid_authentic_secret_request(self, action): + result = node.state_transition(self.chain_state, action) + if action.payment_identifier not in self.failed_secret_requests: + assert events_include(result.events, EventPaymentSentFailed) + else: + assert not result.events + self.failed_secret_requests.add(action.payment_identifier) + + @rule(target=unauthentic_secret_requests, previous_action=init_initiators, secret=secret()) + def secret_request_with_wrong_secrethash(self, previous_action, secret): + assume(sha3(secret) != sha3(previous_action.transfer.secret)) + transfer = deepcopy(previous_action.transfer) + transfer.secret = secret + return self._receive_secret_request(transfer) - @rule(transfer=pending_transfers) - def valid_secret_request(self, transfer): - action = self._receive_secret_request(transfer) + @rule( + target=unauthentic_secret_requests, + previous_action=init_initiators, + payment_identifier=integers(), + ) + def secret_request_with_wrong_payment_id(self, previous_action, payment_identifier): + assume(payment_identifier != previous_action.transfer.payment_identifier) + transfer = deepcopy(previous_action.transfer) + transfer.payment_identifier = payment_identifier + return self._receive_secret_request(transfer) + + @rule(action=unauthentic_secret_requests) + def unauthentic_secret_request(self, action): result = node.state_transition(self.chain_state, action) - assert event_types_match(result.events, [SendSecretReveal]) + assert not result.events TestInitiator = InitiatorState.TestCase + + +@pytest.mark.skip('AssertionError in the tested code (lock is already registered)') +# The firing assertion is commented: "The caller must ensure the same lock is not being used +# twice." It still looks like something that could happen (A transfer initiation is retried +# directly after the first was answered with a secret request.) +def test_failing_path_2(): + state = InitiatorState() + state.failing_path_2 = True + state.initialize(block_number=1, random=Random(), random_seed=None) + v1 = state.populate_transfer_descriptions(amount=1, payment_id=1, secret=b'\x00' * 32) + v2 = state.valid_init_initiator(transfer=v1) + v3 = state.wrong_amount_secret_request(amount=0, previous_action=v2) + state.invalid_authentic_secret_request(v3) + state.valid_init_initiator(transfer=v1) + state.teardown() + + +@pytest.mark.skip('Previous invalid secret request keeps valid one from being processed') +# When processing the invalid secret request, the InitiatorTask is cleared from the dict +# in transfer/node.py:200, which results in the following valid secret request not being +# processed. Is this intentional? +def test_failing_path_4(): + state = InitiatorState() + state.failing_path_4 = True + state.initialize(block_number=1, random=Random(), random_seed=None) + v1 = state.populate_transfer_descriptions(amount=1, payment_id=1, secret=b'\x00' * 32) + v2 = state.valid_init_initiator(transfer=v1) + v3 = state.wrong_amount_secret_request(amount=0, previous_action=v2) + state.invalid_authentic_secret_request(action=v3) + state.valid_secret_request(previous_action=v2) + state.teardown() From d4434ef4b113b88032b014607f9cd6ae55dc5f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20M=C3=BCller?= Date: Wed, 7 Nov 2018 11:00:23 +0100 Subject: [PATCH 4/7] Miscellaneous changes for fuzz testing - wrap hypothesis' event function - change strategy for secret generation - add invariant checks - enable creating multiple channels --- ...itiator_state.py => test_state_changes.py} | 139 +++++++++++++----- 1 file changed, 105 insertions(+), 34 deletions(-) rename raiden/tests/unit/fuzz/{test_initiator_state.py => test_state_changes.py} (62%) diff --git a/raiden/tests/unit/fuzz/test_initiator_state.py b/raiden/tests/unit/fuzz/test_state_changes.py similarity index 62% rename from raiden/tests/unit/fuzz/test_initiator_state.py rename to raiden/tests/unit/fuzz/test_state_changes.py index 0efd653979..29770c509d 100644 --- a/raiden/tests/unit/fuzz/test_initiator_state.py +++ b/raiden/tests/unit/fuzz/test_state_changes.py @@ -1,10 +1,18 @@ +from collections import Counter from copy import deepcopy from random import Random import pytest -from hypothesis import assume -from hypothesis.stateful import Bundle, RuleBasedStateMachine, initialize, rule -from hypothesis.strategies import binary, composite, integers, random_module, randoms +from hypothesis import assume, event +from hypothesis.stateful import ( + Bundle, + RuleBasedStateMachine, + initialize, + invariant, + precondition, + rule, +) +from hypothesis.strategies import builds, composite, integers, random_module, randoms from raiden.constants import GENESIS_BLOCK_NUMBER from raiden.tests.utils import factories @@ -18,22 +26,29 @@ ) from raiden.transfer.state import ChainState, PaymentNetworkState, TokenNetworkState from raiden.transfer.state_change import ContractReceiveChannelNew -from raiden.utils import sha3 +from raiden.utils import random_secret, sha3 @composite def secret(draw): - return draw(binary(min_size=32, max_size=32)) + return draw(builds(random_secret)) -def events_include(events, type, number=1): - return len([event for event in events if isinstance(event, type)]) == number +def event_types_match(events, *expected_types): + return Counter([type(event) for event in events]) == Counter(expected_types) class ChainStateStateMachine(RuleBasedStateMachine): + def __init__(self, address=None, channels_with=None): + self.address = address or factories.make_address() + self.channels_with = channels_with or [factories.make_address()] + self.replay_path = False + self.channels = None + super().__init__() + @initialize( - block_number=integers(min_value=GENESIS_BLOCK_NUMBER), + block_number=integers(min_value=GENESIS_BLOCK_NUMBER + 1), random=randoms(), random_seed=random_module(), ) @@ -65,19 +80,69 @@ def initialize(self, block_number, random, random_seed): self.payment_network_id ] = self.payment_network_state - self.channel = factories.make_channel( - our_balance=1000, - token_network_identifier=self.token_network_id, - ) - - channel_new_state_change = ContractReceiveChannelNew( - factories.make_transaction_hash(), - self.token_network_id, - self.channel, - self.block_number, - ) - - node.state_transition(self.chain_state, channel_new_state_change) + self.channels = list() + + for partner_address in self.channels_with: + channel = factories.make_channel( + our_balance=1000, + partner_balance=1000, + token_network_identifier=self.token_network_id, + our_address=self.address, + partner_address=partner_address, + ) + channel_new_state_change = ContractReceiveChannelNew( + factories.make_transaction_hash(), + self.token_network_id, + channel, + self.block_number, + ) + node.state_transition(self.chain_state, channel_new_state_change) + + self.channels.append(channel) + + def event(self, description): + """ Wrapper for hypothesis' event function. + + hypothesis.event raises an exception when invoked outside of hypothesis + context, so skip it when we are replaying a failed path. + """ + if not self.replay_path: + event(description) + + @precondition(lambda self: self.channels) + @invariant() + def channel_state_invariants(self): + for netting_channel in self.channels: + our_state = netting_channel.our_state + partner_state = netting_channel.partner_state + + our_transferred_amount = 0 + if our_state.balance_proof: + our_transferred_amount = our_state.balance_proof.transferred_amount + assert our_transferred_amount >= 0 + + partner_transferred_amount = 0 + if partner_state.balance_proof: + partner_transferred_amount = partner_state.balance_proof.transferred_amount + assert partner_transferred_amount >= 0 + + assert channel.get_distributable(our_state, partner_state) >= 0 + assert channel.get_distributable(partner_state, our_state) >= 0 + + our_deposit = netting_channel.our_total_deposit + our_amount_locked = channel.get_amount_locked(our_state) + our_balance = channel.get_balance(our_state, partner_state) + assert 0 <= our_amount_locked <= our_balance <= our_deposit + + partner_deposit = netting_channel.partner_total_deposit + partner_amount_locked = channel.get_amount_locked(partner_state) + partner_balance = channel.get_balance(partner_state, our_state) + assert 0 <= partner_amount_locked <= partner_balance <= partner_deposit + + our_net_value = partner_transferred_amount - our_transferred_amount + deposit_sum = our_deposit + partner_deposit + assert 0 <= our_deposit + our_net_value - our_amount_locked <= deposit_sum + assert 0 <= partner_deposit - our_net_value - partner_amount_locked <= deposit_sum class InitiatorState(ChainStateStateMachine): @@ -90,6 +155,10 @@ def __init__(self): self.failing_path_2 = False self.failing_path_4 = False + @property + def channel(self): + return self.channels[0] + def _action_init_initiator(self, transfer: TransferDescriptionWithSecretState): return ActionInitInitiator( transfer, @@ -142,7 +211,7 @@ def valid_init_initiator(self, transfer): assume(transfer.amount <= self._available_amount()) action = self._action_init_initiator(transfer) result = node.state_transition(self.chain_state, action) - assert events_include(result.events, SendLockedTransfer) + assert event_types_match(result.events, SendLockedTransfer) self.initiated.add(transfer.payment_identifier) return action @@ -152,7 +221,7 @@ def valid_secret_request(self, previous_action): assume(previous_action.transfer.payment_identifier not in self.failed_secret_requests) action = self._receive_secret_request(previous_action.transfer) result = node.state_transition(self.chain_state, action) - assert events_include(result.events, SendSecretReveal) + assert event_types_match(result.events, SendSecretReveal) @rule( target=invalid_authentic_secret_requests, @@ -169,7 +238,7 @@ def wrong_amount_secret_request(self, previous_action, amount): def invalid_authentic_secret_request(self, action): result = node.state_transition(self.chain_state, action) if action.payment_identifier not in self.failed_secret_requests: - assert events_include(result.events, EventPaymentSentFailed) + assert event_types_match(result.events, EventPaymentSentFailed) else: assert not result.events self.failed_secret_requests.add(action.payment_identifier) @@ -207,6 +276,7 @@ def unauthentic_secret_request(self, action): # directly after the first was answered with a secret request.) def test_failing_path_2(): state = InitiatorState() + state.replay_path = True state.failing_path_2 = True state.initialize(block_number=1, random=Random(), random_seed=None) v1 = state.populate_transfer_descriptions(amount=1, payment_id=1, secret=b'\x00' * 32) @@ -217,17 +287,18 @@ def test_failing_path_2(): state.teardown() -@pytest.mark.skip('Previous invalid secret request keeps valid one from being processed') -# When processing the invalid secret request, the InitiatorTask is cleared from the dict -# in transfer/node.py:200, which results in the following valid secret request not being -# processed. Is this intentional? -def test_failing_path_4(): +@pytest.mark.skip() +# Makes the same assertion fail like the test above, but this time a second transfer +# with the same secret is created instead of the same transfer being initiated +# twice. +def test_failing_path_2a(): state = InitiatorState() - state.failing_path_4 = True + state.replay_path = True state.initialize(block_number=1, random=Random(), random_seed=None) v1 = state.populate_transfer_descriptions(amount=1, payment_id=1, secret=b'\x00' * 32) - v2 = state.valid_init_initiator(transfer=v1) - v3 = state.wrong_amount_secret_request(amount=0, previous_action=v2) - state.invalid_authentic_secret_request(action=v3) - state.valid_secret_request(previous_action=v2) + v2 = state.populate_transfer_descriptions(amount=1, payment_id=2, secret=b'\x00' * 32) + v3 = state.valid_init_initiator(transfer=v2) + v4 = state.wrong_amount_secret_request(amount=0, previous_action=v3) + state.invalid_authentic_secret_request(action=v4) + state.valid_init_initiator(transfer=v1) state.teardown() From 1e699b3e7d75de2dda37552627b57068508cbe13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20M=C3=BCller?= Date: Tue, 13 Nov 2018 14:10:35 +0100 Subject: [PATCH 5/7] Correct assumption about invalid secret requests in fuzz test If an invalid but authentic secret request is received, we consider the whole transfer to be failed since the request was likely malicious. A following valid secret request will thus not be successful anymore. [no ci integration] --- raiden/tests/unit/fuzz/test_state_changes.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/raiden/tests/unit/fuzz/test_state_changes.py b/raiden/tests/unit/fuzz/test_state_changes.py index 29770c509d..fdd72dcf64 100644 --- a/raiden/tests/unit/fuzz/test_state_changes.py +++ b/raiden/tests/unit/fuzz/test_state_changes.py @@ -149,11 +149,10 @@ class InitiatorState(ChainStateStateMachine): def __init__(self): super().__init__() - self.failed_secret_requests = set() + self.invalidated_transfer_ids = set() self.initiated = set() self.failing_path_2 = False - self.failing_path_4 = False @property def channel(self): @@ -217,11 +216,14 @@ def valid_init_initiator(self, transfer): @rule(previous_action=init_initiators) def valid_secret_request(self, previous_action): - if not self.failing_path_4: - assume(previous_action.transfer.payment_identifier not in self.failed_secret_requests) action = self._receive_secret_request(previous_action.transfer) result = node.state_transition(self.chain_state, action) - assert event_types_match(result.events, SendSecretReveal) + if previous_action.transfer.payment_identifier in self.invalidated_transfer_ids: + assert not result.events + self.event('Valid SecretRequest dropped due to previous invalid one.') + else: + assert event_types_match(result.events, SendSecretReveal) + self.event('Valid SecretRequest accepted.') @rule( target=invalid_authentic_secret_requests, @@ -237,11 +239,11 @@ def wrong_amount_secret_request(self, previous_action, amount): @rule(action=invalid_authentic_secret_requests) def invalid_authentic_secret_request(self, action): result = node.state_transition(self.chain_state, action) - if action.payment_identifier not in self.failed_secret_requests: + if action.payment_identifier not in self.invalidated_transfer_ids: assert event_types_match(result.events, EventPaymentSentFailed) else: assert not result.events - self.failed_secret_requests.add(action.payment_identifier) + self.invalidated_transfer_ids.add(action.payment_identifier) @rule(target=unauthentic_secret_requests, previous_action=init_initiators, secret=secret()) def secret_request_with_wrong_secrethash(self, previous_action, secret): From f3ae31b6ee5e79bff9eb9f5dca06a20efdf8d934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20M=C3=BCller?= Date: Thu, 15 Nov 2018 12:08:42 +0100 Subject: [PATCH 6/7] Represent replayed transfer initiations in fuzz test properly, unskip regression test (fixed together with an earlier issue), correct invariant checks --- raiden/tests/unit/fuzz/test_state_changes.py | 87 ++++---------------- 1 file changed, 18 insertions(+), 69 deletions(-) diff --git a/raiden/tests/unit/fuzz/test_state_changes.py b/raiden/tests/unit/fuzz/test_state_changes.py index fdd72dcf64..b34de1df96 100644 --- a/raiden/tests/unit/fuzz/test_state_changes.py +++ b/raiden/tests/unit/fuzz/test_state_changes.py @@ -2,7 +2,6 @@ from copy import deepcopy from random import Random -import pytest from hypothesis import assume, event from hypothesis.stateful import ( Bundle, @@ -109,51 +108,15 @@ def event(self, description): if not self.replay_path: event(description) - @precondition(lambda self: self.channels) - @invariant() - def channel_state_invariants(self): - for netting_channel in self.channels: - our_state = netting_channel.our_state - partner_state = netting_channel.partner_state - - our_transferred_amount = 0 - if our_state.balance_proof: - our_transferred_amount = our_state.balance_proof.transferred_amount - assert our_transferred_amount >= 0 - - partner_transferred_amount = 0 - if partner_state.balance_proof: - partner_transferred_amount = partner_state.balance_proof.transferred_amount - assert partner_transferred_amount >= 0 - - assert channel.get_distributable(our_state, partner_state) >= 0 - assert channel.get_distributable(partner_state, our_state) >= 0 - - our_deposit = netting_channel.our_total_deposit - our_amount_locked = channel.get_amount_locked(our_state) - our_balance = channel.get_balance(our_state, partner_state) - assert 0 <= our_amount_locked <= our_balance <= our_deposit - - partner_deposit = netting_channel.partner_total_deposit - partner_amount_locked = channel.get_amount_locked(partner_state) - partner_balance = channel.get_balance(partner_state, our_state) - assert 0 <= partner_amount_locked <= partner_balance <= partner_deposit - - our_net_value = partner_transferred_amount - our_transferred_amount - deposit_sum = our_deposit + partner_deposit - assert 0 <= our_deposit + our_net_value - our_amount_locked <= deposit_sum - assert 0 <= partner_deposit - our_net_value - partner_amount_locked <= deposit_sum - class InitiatorState(ChainStateStateMachine): def __init__(self): super().__init__() - self.invalidated_transfer_ids = set() + self.used_secrets = set() + self.processed_secret_requests = set() self.initiated = set() - self.failing_path_2 = False - @property def channel(self): return self.channels[0] @@ -186,6 +149,8 @@ def _receive_secret_request(self, transfer: TransferDescriptionWithSecretState): secret=secret(), ) def populate_transfer_descriptions(self, payment_id, amount, secret): + assume(secret not in self.used_secrets) + self.used_secrets.add(secret) return TransferDescriptionWithSecretState( payment_network_identifier=self.payment_network_id, payment_identifier=payment_id, @@ -204,26 +169,30 @@ def _available_amount(self): @rule(target=init_initiators, transfer=transfers) def valid_init_initiator(self, transfer): - if not self.failing_path_2: - assume(transfer.payment_identifier not in self.initiated) - assume(not self._secret_in_use(transfer.secret)) + assume(transfer.secret not in self.initiated) assume(transfer.amount <= self._available_amount()) action = self._action_init_initiator(transfer) result = node.state_transition(self.chain_state, action) assert event_types_match(result.events, SendLockedTransfer) - self.initiated.add(transfer.payment_identifier) + self.initiated.add(transfer.secret) return action + @rule(previous_action=init_initiators) + def replay_init_initator(self, previous_action): + result = node.state_transition(self.chain_state, previous_action) + assert not result.events + @rule(previous_action=init_initiators) def valid_secret_request(self, previous_action): action = self._receive_secret_request(previous_action.transfer) result = node.state_transition(self.chain_state, action) - if previous_action.transfer.payment_identifier in self.invalidated_transfer_ids: + if action.secrethash in self.processed_secret_requests: assert not result.events self.event('Valid SecretRequest dropped due to previous invalid one.') else: assert event_types_match(result.events, SendSecretReveal) self.event('Valid SecretRequest accepted.') + self.processed_secret_requests.add(action.secrethash) @rule( target=invalid_authentic_secret_requests, @@ -239,11 +208,11 @@ def wrong_amount_secret_request(self, previous_action, amount): @rule(action=invalid_authentic_secret_requests) def invalid_authentic_secret_request(self, action): result = node.state_transition(self.chain_state, action) - if action.payment_identifier not in self.invalidated_transfer_ids: + if action.secrethash not in self.processed_secret_requests: assert event_types_match(result.events, EventPaymentSentFailed) else: assert not result.events - self.invalidated_transfer_ids.add(action.payment_identifier) + self.processed_secret_requests.add(action.secrethash) @rule(target=unauthentic_secret_requests, previous_action=init_initiators, secret=secret()) def secret_request_with_wrong_secrethash(self, previous_action, secret): @@ -272,35 +241,15 @@ def unauthentic_secret_request(self, action): TestInitiator = InitiatorState.TestCase -@pytest.mark.skip('AssertionError in the tested code (lock is already registered)') -# The firing assertion is commented: "The caller must ensure the same lock is not being used -# twice." It still looks like something that could happen (A transfer initiation is retried -# directly after the first was answered with a secret request.) -def test_failing_path_2(): +def test_regression_malicious_secret_request_handled_properly(): state = InitiatorState() state.replay_path = True - state.failing_path_2 = True + state.initialize(block_number=1, random=Random(), random_seed=None) v1 = state.populate_transfer_descriptions(amount=1, payment_id=1, secret=b'\x00' * 32) v2 = state.valid_init_initiator(transfer=v1) v3 = state.wrong_amount_secret_request(amount=0, previous_action=v2) state.invalid_authentic_secret_request(v3) - state.valid_init_initiator(transfer=v1) - state.teardown() - + state.replay_init_initator(previous_action=v2) -@pytest.mark.skip() -# Makes the same assertion fail like the test above, but this time a second transfer -# with the same secret is created instead of the same transfer being initiated -# twice. -def test_failing_path_2a(): - state = InitiatorState() - state.replay_path = True - state.initialize(block_number=1, random=Random(), random_seed=None) - v1 = state.populate_transfer_descriptions(amount=1, payment_id=1, secret=b'\x00' * 32) - v2 = state.populate_transfer_descriptions(amount=1, payment_id=2, secret=b'\x00' * 32) - v3 = state.valid_init_initiator(transfer=v2) - v4 = state.wrong_amount_secret_request(amount=0, previous_action=v3) - state.invalid_authentic_secret_request(action=v4) - state.valid_init_initiator(transfer=v1) state.teardown() From 1ada9ca4c6f98d89f2d866eddaba4d24231e421d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20M=C3=BCller?= Date: Thu, 15 Nov 2018 18:39:04 +0100 Subject: [PATCH 7/7] Add channel invariants as given in smart contracts spec to fuzz tests [no ci integration] --- raiden/tests/unit/fuzz/test_state_changes.py | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/raiden/tests/unit/fuzz/test_state_changes.py b/raiden/tests/unit/fuzz/test_state_changes.py index b34de1df96..8efdd2a731 100644 --- a/raiden/tests/unit/fuzz/test_state_changes.py +++ b/raiden/tests/unit/fuzz/test_state_changes.py @@ -108,6 +108,53 @@ def event(self, description): if not self.replay_path: event(description) + @precondition(lambda self: self.channels) + @invariant() + def channel_state_invariants(self): + """ Check the invariants for the channel state given in the Raiden specification """ + + for netting_channel in self.channels: + our_state = netting_channel.our_state + partner_state = netting_channel.partner_state + + our_transferred_amount = 0 + if our_state.balance_proof: + our_transferred_amount = our_state.balance_proof.transferred_amount + assert our_transferred_amount >= 0 + + partner_transferred_amount = 0 + if partner_state.balance_proof: + partner_transferred_amount = partner_state.balance_proof.transferred_amount + assert partner_transferred_amount >= 0 + + assert channel.get_distributable(our_state, partner_state) >= 0 + assert channel.get_distributable(partner_state, our_state) >= 0 + + our_deposit = netting_channel.our_total_deposit + partner_deposit = netting_channel.partner_total_deposit + total_deposit = our_deposit + partner_deposit + + our_amount_locked = channel.get_amount_locked(our_state) + our_balance = channel.get_balance(our_state, partner_state) + partner_amount_locked = channel.get_amount_locked(partner_state) + partner_balance = channel.get_balance(partner_state, our_state) + + # invariant (5.1R), add withdrawn amounts when implemented + assert 0 <= our_amount_locked <= our_balance + assert 0 <= partner_amount_locked <= partner_balance + assert our_amount_locked <= total_deposit + assert partner_amount_locked <= total_deposit + + our_transferred = partner_transferred_amount - our_transferred_amount + netted_transferred = our_transferred + partner_amount_locked - our_amount_locked + + # invariant (6R), add withdrawn amounts when implemented + assert 0 <= our_deposit + our_transferred - our_amount_locked <= total_deposit + assert 0 <= partner_deposit - our_transferred - partner_amount_locked <= total_deposit + + # invariant (7R), add withdrawn amounts when implemented + assert - our_deposit <= netted_transferred <= partner_deposit + class InitiatorState(ChainStateStateMachine):