From b52050256c148c938f106eaf18472178ea0fb998 Mon Sep 17 00:00:00 2001 From: Gabriel Levcovitz Date: Fri, 5 Jan 2024 20:21:25 -0300 Subject: [PATCH] feat(merged-mining): configure new max merkle path length on testnet --- hathor/conf/settings.py | 4 ++ hathor/conf/testnet.py | 13 ++++ hathor/conf/testnet.yml | 9 +++ hathor/feature_activation/feature.py | 2 + hathor/merged_mining/coordinator.py | 5 +- hathor/transaction/aux_pow.py | 9 +-- .../merge_mined_block_verifier.py | 21 ++++++- hathor/verification/vertex_verifiers.py | 2 +- tests/tx/test_tx.py | 62 ++++++++++++++++--- 9 files changed, 110 insertions(+), 17 deletions(-) diff --git a/hathor/conf/settings.py b/hathor/conf/settings.py index 09a90dd2d..0e93ce4ed 100644 --- a/hathor/conf/settings.py +++ b/hathor/conf/settings.py @@ -419,6 +419,10 @@ def GENESIS_TX2_TIMESTAMP(self) -> int: # Time in seconds to request the best blockchain from peers. BEST_BLOCKCHAIN_INTERVAL: int = 5 # seconds + # Merged mining settings. The old value is going to be replaced by the new value through Feature Activation. + OLD_MAX_MERKLE_PATH_LENGTH: int = 12 + NEW_MAX_MERKLE_PATH_LENGTH: int = 15 + @classmethod def from_yaml(cls, *, filepath: str) -> 'HathorSettings': """Takes a filepath to a yaml file and returns a validated HathorSettings instance.""" diff --git a/hathor/conf/testnet.py b/hathor/conf/testnet.py index bfde279dc..6464e8d73 100644 --- a/hathor/conf/testnet.py +++ b/hathor/conf/testnet.py @@ -14,6 +14,8 @@ from hathor.checkpoint import Checkpoint as cp from hathor.conf.settings import HathorSettings +from hathor.feature_activation.feature import Feature +from hathor.feature_activation.model.criteria import Criteria from hathor.feature_activation.settings import Settings as FeatureActivationSettings SETTINGS = HathorSettings( @@ -54,5 +56,16 @@ ], FEATURE_ACTIVATION=FeatureActivationSettings( default_threshold=15_120, # 15120 = 75% of evaluation_interval (20160) + features={ + Feature.INCREASE_MAX_MERKLE_PATH_LENGTH: Criteria( + bit=3, + start_height=3_507_840, # N (right now the best block is 3_463_000 on testnet) + timeout_height=3_588_480, # N + 2 * 40320 (4 weeks after the start) + minimum_activation_height=0, + lock_in_on_timeout=False, + version='0.59.0', + signal_support_by_default=True, + ) + } ) ) diff --git a/hathor/conf/testnet.yml b/hathor/conf/testnet.yml index d3be15dde..d80ed15b1 100644 --- a/hathor/conf/testnet.yml +++ b/hathor/conf/testnet.yml @@ -38,3 +38,12 @@ CHECKPOINTS: FEATURE_ACTIVATION: default_threshold: 15_120 # 15120 = 75% of evaluation_interval (20160) + features: + INCREASE_MAX_MERKLE_PATH_LENGTH: + bit: 3 + start_height: 3_507_840 # N (right now the best block is 3_463_000 on testnet) + timeout_height: 3_588_480 # N + 2 * 40320 (4 weeks after the start) + minimum_activation_height: 0 + lock_in_on_timeout: false + version: 0.59.0 + signal_support_by_default: true diff --git a/hathor/feature_activation/feature.py b/hathor/feature_activation/feature.py index eb6a6e897..56082def8 100644 --- a/hathor/feature_activation/feature.py +++ b/hathor/feature_activation/feature.py @@ -33,3 +33,5 @@ class Feature(Enum): NOP_FEATURE_4 = 'NOP_FEATURE_4' NOP_FEATURE_5 = 'NOP_FEATURE_5' NOP_FEATURE_6 = 'NOP_FEATURE_6' + + INCREASE_MAX_MERKLE_PATH_LENGTH = 'INCREASE_MAX_MERKLE_PATH_LENGTH' diff --git a/hathor/merged_mining/coordinator.py b/hathor/merged_mining/coordinator.py index 61c9c2a65..9d0fba255 100644 --- a/hathor/merged_mining/coordinator.py +++ b/hathor/merged_mining/coordinator.py @@ -624,7 +624,10 @@ def handle_submit(self, params: list[Any], msgid: Optional[str]) -> None: try: aux_pow = job.build_aux_pow(work) - aux_pow.verify(block_base_hash) + # This constant usually comes from full node settings, but it's hardcoded here as this runs as a + # standalone CLI tool. + old_max_merkle_path_length = 12 + aux_pow.verify(block_base_hash, old_max_merkle_path_length) except TxValidationError as e: self.log.warn('invalid work', job_id=work.job_id, error=e) self.send_error(INVALID_SOLUTION, data={'message': 'Job has invalid work.'}) diff --git a/hathor/transaction/aux_pow.py b/hathor/transaction/aux_pow.py index 0a18ee2aa..72ecf10a2 100644 --- a/hathor/transaction/aux_pow.py +++ b/hathor/transaction/aux_pow.py @@ -19,9 +19,6 @@ logger = get_logger() -MAX_MERKLE_PATH_LENGTH: int = 12 - - class BitcoinAuxPow(NamedTuple): header_head: bytes # 36 bytes coinbase_head: bytes # variable length (at least 47 bytes) @@ -44,7 +41,7 @@ def calculate_hash(self, base_block_hash: bytes) -> bytes: merkle_root = bytes(reversed(build_merkle_root_from_path([coinbase_tx_hash] + self.merkle_path))) return sha256d_hash(self.header_head + merkle_root + self.header_tail) - def verify(self, _base_block_hash: bytes) -> None: + def verify(self, _base_block_hash: bytes, max_merkle_path_length: int) -> None: """ Check for inconsistencies, raises instance of TxValidationError on error. """ from hathor.merged_mining import MAGIC_NUMBER @@ -59,8 +56,8 @@ def verify(self, _base_block_hash: bytes) -> None: if magic_index < len(self.coinbase_head) - len(MAGIC_NUMBER): raise AuxPowUnexpectedMagicError('unexpected MAGIC_NUMBER') merkle_path_length = len(self.merkle_path) - if merkle_path_length > MAX_MERKLE_PATH_LENGTH: - raise AuxPowLongMerklePathError(f'merkle_path too long: {merkle_path_length} > {MAX_MERKLE_PATH_LENGTH}') + if merkle_path_length > max_merkle_path_length: + raise AuxPowLongMerklePathError(f'merkle_path too long: {merkle_path_length} > {max_merkle_path_length}') def __bytes__(self) -> bytes: """ Convert to byte representation. diff --git a/hathor/verification/merge_mined_block_verifier.py b/hathor/verification/merge_mined_block_verifier.py index 9314fbb2a..60bfb42da 100644 --- a/hathor/verification/merge_mined_block_verifier.py +++ b/hathor/verification/merge_mined_block_verifier.py @@ -12,14 +12,31 @@ # See the License for the specific language governing permissions and # limitations under the License. +from hathor.conf.settings import HathorSettings +from hathor.feature_activation.feature import Feature +from hathor.feature_activation.feature_service import FeatureService from hathor.transaction import MergeMinedBlock class MergeMinedBlockVerifier: - __slots__ = () + __slots__ = ('_settings', '_feature_service',) + + def __init__(self, *, settings: HathorSettings, feature_service: FeatureService): + self._settings = settings + self._feature_service = feature_service def verify_aux_pow(self, block: MergeMinedBlock) -> None: """ Verify auxiliary proof-of-work (for merged mining). """ assert block.aux_pow is not None - block.aux_pow.verify(block.get_base_hash()) + + is_feature_active = self._feature_service.is_feature_active( + block=block, + feature=Feature.INCREASE_MAX_MERKLE_PATH_LENGTH + ) + max_merkle_path_length = ( + self._settings.NEW_MAX_MERKLE_PATH_LENGTH if is_feature_active + else self._settings.OLD_MAX_MERKLE_PATH_LENGTH + ) + + block.aux_pow.verify(block.get_base_hash(), max_merkle_path_length) diff --git a/hathor/verification/vertex_verifiers.py b/hathor/verification/vertex_verifiers.py index 339230acb..98477c397 100644 --- a/hathor/verification/vertex_verifiers.py +++ b/hathor/verification/vertex_verifiers.py @@ -66,7 +66,7 @@ def create( Create a VertexVerifiers instance using a custom vertex_verifier. """ block_verifier = BlockVerifier(settings=settings, daa=daa, feature_service=feature_service) - merge_mined_block_verifier = MergeMinedBlockVerifier() + merge_mined_block_verifier = MergeMinedBlockVerifier(settings=settings, feature_service=feature_service) tx_verifier = TransactionVerifier(settings=settings, daa=daa) token_creation_tx_verifier = TokenCreationTransactionVerifier(settings=settings) diff --git a/tests/tx/test_tx.py b/tests/tx/test_tx.py index fd802c7f5..9ebf999bd 100644 --- a/tests/tx/test_tx.py +++ b/tests/tx/test_tx.py @@ -1,9 +1,12 @@ import base64 import hashlib from math import isinf, isnan +from unittest.mock import patch from hathor.crypto.util import decode_address, get_address_from_public_key, get_private_key_from_bytes from hathor.daa import TestMode +from hathor.feature_activation.feature import Feature +from hathor.feature_activation.feature_service import FeatureService from hathor.simulator.utils import add_new_blocks from hathor.transaction import MAX_OUTPUT_VALUE, Block, Transaction, TxInput, TxOutput from hathor.transaction.exceptions import ( @@ -222,15 +225,19 @@ def test_merge_mined_no_magic(self): from hathor.transaction.exceptions import AuxPowNoMagicError from hathor.transaction.merge_mined_block import MergeMinedBlock - parents = [tx.hash for tx in self.genesis] + parent_block = self.genesis_blocks[0].hash + parent_txs = [tx.hash for tx in self.genesis_txs] + parents = [parent_block, *parent_txs] address = decode_address(self.get_address(1)) outputs = [TxOutput(100, P2PKH.create_output_script(address))] b = MergeMinedBlock( + hash=b'some_hash', timestamp=self.genesis_blocks[0].timestamp + 1, weight=1, outputs=outputs, parents=parents, + storage=self.tx_storage, aux_pow=BitcoinAuxPow( b'\x00' * 32, b'\x00' * 42, # no MAGIC_NUMBER @@ -253,7 +260,9 @@ def test_merge_mined_multiple_magic(self): from hathor.transaction.exceptions import AuxPowUnexpectedMagicError from hathor.transaction.merge_mined_block import MergeMinedBlock - parents = [tx.hash for tx in self.genesis] + parent_block = self.genesis_blocks[0].hash + parent_txs = [tx.hash for tx in self.genesis_txs] + parents = [parent_block, *parent_txs] address1 = decode_address(self.get_address(1)) address2 = decode_address(self.get_address(2)) assert address1 != address2 @@ -261,17 +270,21 @@ def test_merge_mined_multiple_magic(self): outputs2 = [TxOutput(100, P2PKH.create_output_script(address2))] b1 = MergeMinedBlock( + hash=b'some_hash1', timestamp=self.genesis_blocks[0].timestamp + 1, weight=1, outputs=outputs1, parents=parents, + storage=self.tx_storage, ) b2 = MergeMinedBlock( + hash=b'some_hash2', timestamp=self.genesis_blocks[0].timestamp + 1, weight=1, outputs=outputs2, parents=parents, + storage=self.tx_storage, ) assert b1.get_base_hash() != b2.get_base_hash() @@ -321,6 +334,16 @@ def test_merge_mined_long_merkle_path(self): address = decode_address(self.get_address(1)) outputs = [TxOutput(100, P2PKH.create_output_script(address))] + patch_path = 'hathor.feature_activation.feature_service.FeatureService.is_feature_active' + + def is_feature_active_false(self: FeatureService, *, block: Block, feature: Feature) -> bool: + assert feature == Feature.INCREASE_MAX_MERKLE_PATH_LENGTH + return False + + def is_feature_active_true(self: FeatureService, *, block: Block, feature: Feature) -> bool: + assert feature == Feature.INCREASE_MAX_MERKLE_PATH_LENGTH + return True + b = MergeMinedBlock( timestamp=self.genesis_blocks[0].timestamp + 1, weight=1, @@ -330,17 +353,42 @@ def test_merge_mined_long_merkle_path(self): b'\x00' * 32, b'\x00' * 42 + MAGIC_NUMBER, b'\x00' * 18, - [b'\x00' * 32] * 13, # 1 too long + [b'\x00' * 32] * (self._settings.OLD_MAX_MERKLE_PATH_LENGTH + 1), # 1 too long b'\x00' * 12, ) ) - with self.assertRaises(AuxPowLongMerklePathError): + # Test with the INCREASE_MAX_MERKLE_PATH_LENGTH feature disabled + with patch(patch_path, is_feature_active_false): + with self.assertRaises(AuxPowLongMerklePathError): + self._verifiers.merge_mined_block.verify_aux_pow(b) + + # removing one path makes it work + b.aux_pow.merkle_path.pop() self._verifiers.merge_mined_block.verify_aux_pow(b) - # removing one path makes it work - b.aux_pow.merkle_path.pop() - self._verifiers.merge_mined_block.verify_aux_pow(b) + b2 = MergeMinedBlock( + timestamp=self.genesis_blocks[0].timestamp + 1, + weight=1, + outputs=outputs, + parents=parents, + aux_pow=BitcoinAuxPow( + b'\x00' * 32, + b'\x00' * 42 + MAGIC_NUMBER, + b'\x00' * 18, + [b'\x00' * 32] * (self._settings.NEW_MAX_MERKLE_PATH_LENGTH + 1), # 1 too long + b'\x00' * 12, + ) + ) + + # Test with the INCREASE_MAX_MERKLE_PATH_LENGTH feature enabled + with patch(patch_path, is_feature_active_true): + with self.assertRaises(AuxPowLongMerklePathError): + self._verifiers.merge_mined_block.verify_aux_pow(b2) + + # removing one path makes it work + b2.aux_pow.merkle_path.pop() + self._verifiers.merge_mined_block.verify_aux_pow(b2) def test_block_outputs(self): from hathor.transaction.exceptions import TooManyOutputs