diff --git a/hathor/transaction/base_transaction.py b/hathor/transaction/base_transaction.py index 8615b099f..ec457c680 100644 --- a/hathor/transaction/base_transaction.py +++ b/hathor/transaction/base_transaction.py @@ -42,8 +42,9 @@ TxValidationError, WeightError, ) -from hathor.transaction.transaction_metadata import TransactionMetadata, ValidationState +from hathor.transaction.transaction_metadata import TransactionMetadata from hathor.transaction.util import VerboseCallback, int_to_bytes, unpack, unpack_len +from hathor.transaction.validation_state import ValidationState from hathor.util import classproperty if TYPE_CHECKING: diff --git a/hathor/transaction/storage/transaction_storage.py b/hathor/transaction/storage/transaction_storage.py index 14ae9b2a5..8970cc8ee 100644 --- a/hathor/transaction/storage/transaction_storage.py +++ b/hathor/transaction/storage/transaction_storage.py @@ -1025,7 +1025,7 @@ def iter_mempool_from_best_index(self) -> Iterator[Transaction]: def compute_transactions_that_became_invalid(self) -> List[BaseTransaction]: """ This method will look for transactions in the mempool that have became invalid due to the reward lock. """ - from hathor.transaction.transaction_metadata import ValidationState + from hathor.transaction.validation_state import ValidationState to_remove: List[BaseTransaction] = [] for tx in self.iter_mempool_from_best_index(): if tx.is_spent_reward_locked(): diff --git a/hathor/transaction/transaction_metadata.py b/hathor/transaction/transaction_metadata.py index f2cec5ad6..9f70cff38 100644 --- a/hathor/transaction/transaction_metadata.py +++ b/hathor/transaction/transaction_metadata.py @@ -13,9 +13,9 @@ # limitations under the License. from collections import defaultdict -from enum import IntEnum, unique from typing import TYPE_CHECKING, Any, Dict, FrozenSet, List, Optional, Set +from hathor.transaction.validation_state import ValidationState from hathor.util import practically_equal if TYPE_CHECKING: @@ -25,80 +25,6 @@ from hathor.transaction.storage import TransactionStorage -@unique -class ValidationState(IntEnum): - """ - - Possible transitions: - - - Initial - -> Basic: parents exist, graph information checks-out - -> Invalid: all information to reach `Basic` was available, but something doesn't check out - -> Checkpoint: is a block which hash matches a known checkpoint is a parent of a Checkpoint-valid tx - - Basic - -> Full: all parents reached `Full`, and validation+consensus ran successfully - -> Invalid: all information to reach `Full` was available, but something doesn't check out - - Checkpoint - -> Checkpoint-Full: when all the chain of parents and inputs up to the genesis exist in the database - - Full: final - - Checkpoint-Full: final - - Invalid: final - - `BASIC` means only the validations that can run without access to the dependencies (parents+inputs, except for - blocks the block parent has to exist and be at least BASIC) have been run. For example, if it's `BASIC` the weight - of a tx has been validated and is correct, but it may be spending a tx that has already been spent, we will not run - this validation until _all_ the dependencies have reached `FULL` or any of them `INVALID` (which should - automatically invalidate this tx). In theory it should be possible to have even more granular validation (if one of - the inputs exists, validate that we can spend it), but the complexity for that is too high. - - """ - INITIAL = 0 # aka, not validated - BASIC = 1 # only graph info has been validated - CHECKPOINT = 2 # validation can be safely assumed because it traces up to a known checkpoint - FULL = 3 # fully validated - CHECKPOINT_FULL = 4 # besides being checkpoint valid, it is fully connected - INVALID = -1 # not valid, this does not mean not best chain, orphan chains can be valid - - def is_initial(self) -> bool: - """Short-hand property""" - return self is ValidationState.INITIAL - - def is_at_least_basic(self) -> bool: - """Until a validation is final, it is possible to change its state when more information is available.""" - return self >= ValidationState.BASIC - - def is_valid(self) -> bool: - """Short-hand property.""" - return self in {ValidationState.FULL, ValidationState.CHECKPOINT, ValidationState.CHECKPOINT_FULL} - - def is_checkpoint(self) -> bool: - """Short-hand property.""" - return self in {ValidationState.CHECKPOINT, ValidationState.CHECKPOINT_FULL} - - def is_fully_connected(self) -> bool: - """Short-hand property.""" - return self in {ValidationState.FULL, ValidationState.CHECKPOINT_FULL} - - def is_partial(self) -> bool: - """Short-hand property.""" - return self in {ValidationState.INITIAL, ValidationState.BASIC, ValidationState.CHECKPOINT} - - def is_invalid(self) -> bool: - """Short-hand property.""" - return self is ValidationState.INVALID - - def is_final(self) -> bool: - """Until a validation is final, it is possible to change its state when more information is available.""" - return self in {ValidationState.FULL, ValidationState.CHECKPOINT_FULL, ValidationState.INVALID} - - @classmethod - def from_name(cls, name: str) -> 'ValidationState': - value = getattr(cls, name.upper(), None) - if value is None: - raise ValueError('invalid name') - return value - - class TransactionMetadata: hash: Optional[bytes] spent_outputs: Dict[int, List[bytes]] diff --git a/hathor/transaction/validation_state.py b/hathor/transaction/validation_state.py new file mode 100644 index 000000000..be4d52fcd --- /dev/null +++ b/hathor/transaction/validation_state.py @@ -0,0 +1,89 @@ +# Copyright 2023 Hathor Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import IntEnum, unique + + +@unique +class ValidationState(IntEnum): + """ + + Possible transitions: + + - Initial + -> Basic: parents exist, graph information checks-out + -> Invalid: all information to reach `Basic` was available, but something doesn't check out + -> Checkpoint: is a block which hash matches a known checkpoint is a parent of a Checkpoint-valid tx + - Basic + -> Full: all parents reached `Full`, and validation+consensus ran successfully + -> Invalid: all information to reach `Full` was available, but something doesn't check out + - Checkpoint + -> Checkpoint-Full: when all the chain of parents and inputs up to the genesis exist in the database + - Full: final + - Checkpoint-Full: final + - Invalid: final + + `BASIC` means only the validations that can run without access to the dependencies (parents+inputs, except for + blocks the block parent has to exist and be at least BASIC) have been run. For example, if it's `BASIC` the weight + of a tx has been validated and is correct, but it may be spending a tx that has already been spent, we will not run + this validation until _all_ the dependencies have reached `FULL` or any of them `INVALID` (which should + automatically invalidate this tx). In theory it should be possible to have even more granular validation (if one of + the inputs exists, validate that we can spend it), but the complexity for that is too high. + + """ + INITIAL = 0 # aka, not validated + BASIC = 1 # only graph info has been validated + CHECKPOINT = 2 # validation can be safely assumed because it traces up to a known checkpoint + FULL = 3 # fully validated + CHECKPOINT_FULL = 4 # besides being checkpoint valid, it is fully connected + INVALID = -1 # not valid, this does not mean not best chain, orphan chains can be valid + + def is_initial(self) -> bool: + """Short-hand property""" + return self is ValidationState.INITIAL + + def is_at_least_basic(self) -> bool: + """Until a validation is final, it is possible to change its state when more information is available.""" + return self >= ValidationState.BASIC + + def is_valid(self) -> bool: + """Short-hand property.""" + return self in {ValidationState.FULL, ValidationState.CHECKPOINT, ValidationState.CHECKPOINT_FULL} + + def is_checkpoint(self) -> bool: + """Short-hand property.""" + return self in {ValidationState.CHECKPOINT, ValidationState.CHECKPOINT_FULL} + + def is_fully_connected(self) -> bool: + """Short-hand property.""" + return self in {ValidationState.FULL, ValidationState.CHECKPOINT_FULL} + + def is_partial(self) -> bool: + """Short-hand property.""" + return self in {ValidationState.INITIAL, ValidationState.BASIC, ValidationState.CHECKPOINT} + + def is_invalid(self) -> bool: + """Short-hand property.""" + return self is ValidationState.INVALID + + def is_final(self) -> bool: + """Until a validation is final, it is possible to change its state when more information is available.""" + return self in {ValidationState.FULL, ValidationState.CHECKPOINT_FULL, ValidationState.INVALID} + + @classmethod + def from_name(cls, name: str) -> 'ValidationState': + value = getattr(cls, name.upper(), None) + if value is None: + raise ValueError('invalid name') + return value diff --git a/tests/resources/transaction/test_tx.py b/tests/resources/transaction/test_tx.py index 1fe8bbc23..d5cc377e8 100644 --- a/tests/resources/transaction/test_tx.py +++ b/tests/resources/transaction/test_tx.py @@ -3,7 +3,7 @@ from hathor.transaction import Transaction from hathor.transaction.resources import TransactionResource from hathor.transaction.token_creation_tx import TokenCreationTransaction -from hathor.transaction.transaction_metadata import ValidationState +from hathor.transaction.validation_state import ValidationState from tests import unittest from tests.resources.base_resource import StubSite, _BaseResourceTest from tests.utils import add_blocks_unlock_reward, add_new_blocks, add_new_transactions diff --git a/tests/tx/test_cache_storage.py b/tests/tx/test_cache_storage.py index cb4461624..bae11ca40 100644 --- a/tests/tx/test_cache_storage.py +++ b/tests/tx/test_cache_storage.py @@ -31,7 +31,7 @@ def tearDown(self): super().tearDown() def _get_new_tx(self, nonce): - from hathor.transaction.transaction_metadata import ValidationState + from hathor.transaction.validation_state import ValidationState tx = Transaction(nonce=nonce, storage=self.cache_storage) tx.update_hash() meta = TransactionMetadata(hash=tx.hash) diff --git a/tests/tx/test_tx.py b/tests/tx/test_tx.py index 1b1ee1a63..59648342f 100644 --- a/tests/tx/test_tx.py +++ b/tests/tx/test_tx.py @@ -30,8 +30,8 @@ ) from hathor.transaction.scripts import P2PKH, parse_address_script from hathor.transaction.storage import TransactionMemoryStorage -from hathor.transaction.transaction_metadata import ValidationState from hathor.transaction.util import int_to_bytes +from hathor.transaction.validation_state import ValidationState from hathor.wallet import Wallet from tests import unittest from tests.utils import ( diff --git a/tests/tx/test_tx_storage.py b/tests/tx/test_tx_storage.py index e5a749959..84b600fe8 100644 --- a/tests/tx/test_tx_storage.py +++ b/tests/tx/test_tx_storage.py @@ -16,7 +16,7 @@ from hathor.transaction.scripts import P2PKH from hathor.transaction.storage import TransactionCacheStorage, TransactionMemoryStorage, TransactionRocksDBStorage from hathor.transaction.storage.exceptions import TransactionDoesNotExist -from hathor.transaction.transaction_metadata import ValidationState +from hathor.transaction.validation_state import ValidationState from tests.unittest import TestBuilder from tests.utils import ( BURN_ADDRESS, diff --git a/tests/tx/test_validation_states.py b/tests/tx/test_validation_states.py index b844690f4..d0e34689b 100644 --- a/tests/tx/test_validation_states.py +++ b/tests/tx/test_validation_states.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from hathor.transaction.transaction_metadata import ValidationState +from hathor.transaction.validation_state import ValidationState def test_validation_states_list_unchanged(): diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index ed05b53a6..d6fdb9a43 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -55,7 +55,7 @@ def test_wallet_keys_storage(self): self.assertEqual(key, key2) def test_wallet_create_transaction(self): - from hathor.transaction.transaction_metadata import ValidationState + from hathor.transaction.validation_state import ValidationState genesis_private_key_bytes = get_private_key_bytes( self.genesis_private_key, diff --git a/tests/wallet/test_wallet_hd.py b/tests/wallet/test_wallet_hd.py index c231053ba..b8807f939 100644 --- a/tests/wallet/test_wallet_hd.py +++ b/tests/wallet/test_wallet_hd.py @@ -25,7 +25,7 @@ def setUp(self): self.TOKENS = self.BLOCK_TOKENS def test_transaction_and_balance(self): - from hathor.transaction.transaction_metadata import ValidationState + from hathor.transaction.validation_state import ValidationState # generate a new block and check if we increase balance new_address = self.wallet.get_unused_address()