From 27b07b13ad08658bfcb88a3a80301cbfdc124fd9 Mon Sep 17 00:00:00 2001 From: Jan Segre Date: Thu, 9 Nov 2023 20:02:06 +0100 Subject: [PATCH] fix(verification): avoid validation downgrade by not running it twice --- hathor/verification/verification_service.py | 4 + tests/tx/test_verification.py | 166 ++++++++++++++++++++ 2 files changed, 170 insertions(+) diff --git a/hathor/verification/verification_service.py b/hathor/verification/verification_service.py index 3ead088bd..3248d6516 100644 --- a/hathor/verification/verification_service.py +++ b/hathor/verification/verification_service.py @@ -66,6 +66,10 @@ def validate_basic(self, vertex: BaseTransaction, *, skip_block_weight_verificat If no exception is raised, the ValidationState will end up as `BASIC` and return `True`. """ + # XXX: skip validation if previously validated + if vertex.get_metadata().validation.is_at_least_basic(): + return True + self.verify_basic(vertex, skip_block_weight_verification=skip_block_weight_verification) vertex.set_validation(ValidationState.BASIC) diff --git a/tests/tx/test_verification.py b/tests/tx/test_verification.py index 6aec9e422..833b23dbb 100644 --- a/tests/tx/test_verification.py +++ b/tests/tx/test_verification.py @@ -200,6 +200,30 @@ def test_block_validate_basic(self) -> None: verify_weight_wrapped.assert_called_once() verify_reward_wrapped.assert_called_once() + # validation should be BASIC + self.assertEqual(block.get_metadata().validation, ValidationState.BASIC) + + # full validation should still pass and the validation updated to FULL + self.manager.verification_service.validate_full(block) + self.assertEqual(block.get_metadata().validation, ValidationState.FULL) + + # and if running basic validation again it shouldn't validate or change the validation state + verify_weight_wrapped2 = Mock(wraps=self.verifiers.block.verify_weight) + verify_reward_wrapped2 = Mock(wraps=self.verifiers.block.verify_reward) + + with ( + patch.object(BlockVerifier, 'verify_weight', verify_weight_wrapped2), + patch.object(BlockVerifier, 'verify_reward', verify_reward_wrapped2), + ): + self.manager.verification_service.validate_basic(block) + + # Block methods + verify_weight_wrapped2.assert_not_called() + verify_reward_wrapped2.assert_not_called() + + # validation should still be FULL, it must not be BASIC + self.assertEqual(block.get_metadata().validation, ValidationState.FULL) + def test_block_validate_full(self) -> None: block = self._get_valid_block() @@ -352,6 +376,30 @@ def test_merge_mined_block_validate_basic(self) -> None: verify_weight_wrapped.assert_called_once() verify_reward_wrapped.assert_called_once() + # validation should be BASIC + self.assertEqual(block.get_metadata().validation, ValidationState.BASIC) + + # full validation should still pass and the validation updated to FULL + self.manager.verification_service.validate_full(block) + self.assertEqual(block.get_metadata().validation, ValidationState.FULL) + + # and if running basic validation again it shouldn't validate or change the validation state + verify_weight_wrapped2 = Mock(wraps=self.verifiers.block.verify_weight) + verify_reward_wrapped2 = Mock(wraps=self.verifiers.block.verify_reward) + + with ( + patch.object(BlockVerifier, 'verify_weight', verify_weight_wrapped2), + patch.object(BlockVerifier, 'verify_reward', verify_reward_wrapped2), + ): + self.manager.verification_service.validate_basic(block) + + # Block methods + verify_weight_wrapped2.assert_not_called() + verify_reward_wrapped2.assert_not_called() + + # validation should still be FULL, it must not be BASIC + self.assertEqual(block.get_metadata().validation, ValidationState.FULL) + def test_merge_mined_block_validate_full(self) -> None: block = self._get_valid_merge_mined_block() @@ -502,6 +550,8 @@ def test_transaction_verify(self) -> None: verify_reward_locked_wrapped.assert_called_once() def test_transaction_validate_basic(self) -> None: + # add enough blocks so that it can be fully validated later on the tests + add_blocks_unlock_reward(self.manager) tx = self._get_valid_tx() verify_parents_basic_wrapped = Mock(wraps=self.verifiers.tx.verify_parents_basic) @@ -532,6 +582,45 @@ def test_transaction_validate_basic(self) -> None: verify_number_of_outputs_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() + # validation should be BASIC + self.assertEqual(tx.get_metadata().validation, ValidationState.BASIC) + + # full validation should still pass and the validation updated to FULL + self.manager.verification_service.validate_full(tx) + self.assertEqual(tx.get_metadata().validation, ValidationState.FULL) + + # and if running basic validation again it shouldn't validate or change the validation state + verify_parents_basic_wrapped2 = Mock(wraps=self.verifiers.tx.verify_parents_basic) + verify_weight_wrapped2 = Mock(wraps=self.verifiers.tx.verify_weight) + verify_pow_wrapped2 = Mock(wraps=self.verifiers.tx.verify_pow) + verify_number_of_inputs_wrapped2 = Mock(wraps=self.verifiers.tx.verify_number_of_inputs) + verify_outputs_wrapped2 = Mock(wraps=self.verifiers.tx.verify_outputs) + verify_number_of_outputs_wrapped2 = Mock(wraps=self.verifiers.tx.verify_number_of_outputs) + verify_sigops_output_wrapped2 = Mock(wraps=self.verifiers.tx.verify_sigops_output) + + with ( + patch.object(TransactionVerifier, 'verify_parents_basic', verify_parents_basic_wrapped2), + patch.object(TransactionVerifier, 'verify_weight', verify_weight_wrapped2), + patch.object(TransactionVerifier, 'verify_pow', verify_pow_wrapped2), + patch.object(TransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped2), + patch.object(TransactionVerifier, 'verify_outputs', verify_outputs_wrapped2), + patch.object(TransactionVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped2), + patch.object(TransactionVerifier, 'verify_sigops_output', verify_sigops_output_wrapped2), + ): + self.manager.verification_service.validate_basic(tx) + + # Transaction methods + verify_parents_basic_wrapped2.assert_not_called() + verify_weight_wrapped2.assert_not_called() + verify_pow_wrapped2.assert_not_called() + verify_number_of_inputs_wrapped2.assert_not_called() + verify_outputs_wrapped2.assert_not_called() + verify_number_of_outputs_wrapped2.assert_not_called() + verify_sigops_output_wrapped2.assert_not_called() + + # validation should still be FULL, it must not be BASIC + self.assertEqual(tx.get_metadata().validation, ValidationState.FULL) + def test_transaction_validate_full(self) -> None: add_blocks_unlock_reward(self.manager) tx = self._get_valid_tx() @@ -582,6 +671,41 @@ def test_transaction_validate_full(self) -> None: verify_sum_wrapped.assert_called_once() verify_reward_locked_wrapped.assert_called_once() + # validation should be FULL + self.assertEqual(tx.get_metadata().validation, ValidationState.FULL) + + # and if running full validation again it shouldn't validate or change the validation state + verify_parents_basic_wrapped2 = Mock(wraps=self.verifiers.tx.verify_parents_basic) + verify_weight_wrapped2 = Mock(wraps=self.verifiers.tx.verify_weight) + verify_pow_wrapped2 = Mock(wraps=self.verifiers.tx.verify_pow) + verify_number_of_inputs_wrapped2 = Mock(wraps=self.verifiers.tx.verify_number_of_inputs) + verify_outputs_wrapped2 = Mock(wraps=self.verifiers.tx.verify_outputs) + verify_number_of_outputs_wrapped2 = Mock(wraps=self.verifiers.tx.verify_number_of_outputs) + verify_sigops_output_wrapped2 = Mock(wraps=self.verifiers.tx.verify_sigops_output) + + with ( + patch.object(TransactionVerifier, 'verify_parents_basic', verify_parents_basic_wrapped2), + patch.object(TransactionVerifier, 'verify_weight', verify_weight_wrapped2), + patch.object(TransactionVerifier, 'verify_pow', verify_pow_wrapped2), + patch.object(TransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped2), + patch.object(TransactionVerifier, 'verify_outputs', verify_outputs_wrapped2), + patch.object(TransactionVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped2), + patch.object(TransactionVerifier, 'verify_sigops_output', verify_sigops_output_wrapped2), + ): + self.manager.verification_service.validate_basic(tx) + + # Transaction methods + verify_parents_basic_wrapped2.assert_not_called() + verify_weight_wrapped2.assert_not_called() + verify_pow_wrapped2.assert_not_called() + verify_number_of_inputs_wrapped2.assert_not_called() + verify_outputs_wrapped2.assert_not_called() + verify_number_of_outputs_wrapped2.assert_not_called() + verify_sigops_output_wrapped2.assert_not_called() + + # validation should still be FULL, it must not be BASIC + self.assertEqual(tx.get_metadata().validation, ValidationState.FULL) + def test_token_creation_transaction_verify_basic(self) -> None: tx = self._get_valid_token_creation_tx() @@ -692,6 +816,7 @@ def test_token_creation_transaction_verify(self) -> None: def test_token_creation_transaction_validate_basic(self) -> None: tx = self._get_valid_token_creation_tx() + tx.get_metadata().validation = ValidationState.INITIAL verify_parents_basic_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_parents_basic) verify_weight_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_weight) @@ -722,6 +847,47 @@ def test_token_creation_transaction_validate_basic(self) -> None: verify_number_of_outputs_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() + # validation should be BASIC + self.assertEqual(tx.get_metadata().validation, ValidationState.BASIC) + + # full validation should still pass and the validation updated to FULL + self.manager.verification_service.validate_full(tx) + self.assertEqual(tx.get_metadata().validation, ValidationState.FULL) + + # and if running basic validation again it shouldn't validate or change the validation state + verify_parents_basic_wrapped2 = Mock(wraps=self.verifiers.token_creation_tx.verify_parents_basic) + verify_weight_wrapped2 = Mock(wraps=self.verifiers.token_creation_tx.verify_weight) + verify_pow_wrapped2 = Mock(wraps=self.verifiers.token_creation_tx.verify_pow) + verify_number_of_inputs_wrapped2 = Mock(wraps=self.verifiers.token_creation_tx.verify_number_of_inputs) + verify_outputs_wrapped2 = Mock(wraps=self.verifiers.token_creation_tx.verify_outputs) + verify_number_of_outputs_wrapped2 = Mock(wraps=self.verifiers.token_creation_tx.verify_number_of_outputs) + verify_sigops_output_wrapped2 = Mock(wraps=self.verifiers.token_creation_tx.verify_sigops_output) + + with ( + patch.object(TokenCreationTransactionVerifier, 'verify_parents_basic', verify_parents_basic_wrapped2), + patch.object(TokenCreationTransactionVerifier, 'verify_weight', verify_weight_wrapped2), + patch.object(TokenCreationTransactionVerifier, 'verify_pow', verify_pow_wrapped2), + patch.object(TokenCreationTransactionVerifier, 'verify_number_of_inputs', + verify_number_of_inputs_wrapped2), + patch.object(TokenCreationTransactionVerifier, 'verify_outputs', verify_outputs_wrapped2), + patch.object(TokenCreationTransactionVerifier, 'verify_number_of_outputs', + verify_number_of_outputs_wrapped2), + patch.object(TokenCreationTransactionVerifier, 'verify_sigops_output', verify_sigops_output_wrapped2), + ): + self.manager.verification_service.validate_basic(tx) + + # Transaction methods + verify_parents_basic_wrapped2.assert_not_called() + verify_weight_wrapped2.assert_not_called() + verify_pow_wrapped2.assert_not_called() + verify_number_of_inputs_wrapped2.assert_not_called() + verify_outputs_wrapped2.assert_not_called() + verify_number_of_outputs_wrapped2.assert_not_called() + verify_sigops_output_wrapped2.assert_not_called() + + # validation should still be FULL, it must not be BASIC + self.assertEqual(tx.get_metadata().validation, ValidationState.FULL) + def test_token_creation_transaction_validate_full(self) -> None: tx = self._get_valid_token_creation_tx() tx.get_metadata().validation = ValidationState.INITIAL