-
Notifications
You must be signed in to change notification settings - Fork 43
refactor(side-dag): general improvements #1080
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ | |
| from hathor.consensus import poa | ||
| from hathor.consensus.consensus_settings import PoaSettings | ||
| from hathor.crypto.util import get_public_key_bytes_compressed | ||
| from hathor.pubsub import EventArguments, HathorEvents | ||
| from hathor.reactor import ReactorProtocol | ||
| from hathor.util import not_none | ||
|
|
||
|
|
@@ -35,11 +36,8 @@ | |
|
|
||
| logger = get_logger() | ||
|
|
||
| # Number of seconds to wait for a sync to finish before trying to produce blocks | ||
| _WAIT_SYNC_DELAY: int = 30 | ||
|
|
||
| # Number of seconds used between each signer depending on its distance to the expected signer | ||
| _SIGNER_TURN_INTERVAL: int = 1 | ||
| _SIGNER_TURN_INTERVAL: int = 10 | ||
|
|
||
|
|
||
| class PoaBlockProducer: | ||
|
|
@@ -54,11 +52,9 @@ class PoaBlockProducer: | |
| '_reactor', | ||
| '_manager', | ||
| '_poa_signer', | ||
| '_started_producing', | ||
| '_start_producing_lc', | ||
| '_schedule_block_lc', | ||
| '_last_seen_best_block', | ||
| '_delayed_call', | ||
| '_start_producing_lc', | ||
| ) | ||
|
|
||
| def __init__(self, *, settings: HathorSettings, reactor: ReactorProtocol, poa_signer: PoaSigner) -> None: | ||
|
|
@@ -70,14 +66,9 @@ def __init__(self, *, settings: HathorSettings, reactor: ReactorProtocol, poa_si | |
| self._manager: HathorManager | None = None | ||
| self._poa_signer = poa_signer | ||
| self._last_seen_best_block: Block | None = None | ||
|
|
||
| self._started_producing = False | ||
| self._start_producing_lc = LoopingCall(self._start_producing) | ||
| self._start_producing_lc.clock = self._reactor | ||
|
|
||
| self._schedule_block_lc = LoopingCall(self._schedule_block) | ||
| self._schedule_block_lc.clock = self._reactor | ||
| self._delayed_call: IDelayedCall | None = None | ||
| self._start_producing_lc: LoopingCall = LoopingCall(self._safe_start_producing) | ||
| self._start_producing_lc.clock = self._reactor | ||
|
|
||
| @property | ||
| def manager(self) -> HathorManager: | ||
|
|
@@ -89,19 +80,18 @@ def manager(self, manager: HathorManager) -> None: | |
| self._manager = manager | ||
|
|
||
| def start(self) -> None: | ||
| self._start_producing_lc.start(_WAIT_SYNC_DELAY) | ||
| self._schedule_block_lc.start(self._settings.AVG_TIME_BETWEEN_BLOCKS) | ||
| self.manager.pubsub.subscribe(HathorEvents.NETWORK_NEW_TX_ACCEPTED, self._on_new_vertex) | ||
| self._start_producing_lc.start(self._settings.AVG_TIME_BETWEEN_BLOCKS) | ||
|
|
||
| def stop(self) -> None: | ||
| if self._start_producing_lc.running: | ||
| self._start_producing_lc.stop() | ||
|
|
||
| if self._schedule_block_lc.running: | ||
| self._schedule_block_lc.stop() | ||
| self.manager.pubsub.unsubscribe(HathorEvents.NETWORK_NEW_TX_ACCEPTED, self._on_new_vertex) | ||
|
|
||
| if self._delayed_call and self._delayed_call.active(): | ||
| self._delayed_call.cancel() | ||
|
|
||
| if self._start_producing_lc.running: | ||
| self._start_producing_lc.stop() | ||
|
|
||
| def _get_signer_index(self, previous_block: Block) -> int | None: | ||
| """Return our signer index considering the active signers.""" | ||
| height = previous_block.get_height() + 1 | ||
|
|
@@ -113,21 +103,49 @@ def _get_signer_index(self, previous_block: Block) -> int | None: | |
| except ValueError: | ||
| return None | ||
|
|
||
| def _start_producing(self) -> None: | ||
| def _safe_start_producing(self) -> None: | ||
| try: | ||
| return self._unsafe_start_producing() | ||
| except Exception: | ||
| self._log.exception('error while trying to start block production') | ||
|
|
||
| def _unsafe_start_producing(self) -> None: | ||
| """Start producing new blocks.""" | ||
| if not self.manager.can_start_mining(): | ||
| # We're syncing, so we'll try again later | ||
| self._log.warn('cannot start producing new blocks, node not synced') | ||
| return | ||
|
|
||
| self._log.info('started producing new blocks') | ||
| self._started_producing = True | ||
| self._start_producing_lc.stop() | ||
| self._schedule_block() | ||
|
|
||
| def _on_new_vertex(self, event: HathorEvents, args: EventArguments) -> None: | ||
| """Handle propagation of new blocks after a vertex is received.""" | ||
| assert event is HathorEvents.NETWORK_NEW_TX_ACCEPTED | ||
| block = args.tx | ||
|
|
||
| from hathor.transaction import Block | ||
| if not isinstance(block, Block): | ||
| return | ||
|
|
||
| from hathor.transaction.poa import PoaBlock | ||
| if isinstance(block, PoaBlock) and not block.weight == poa.BLOCK_WEIGHT_IN_TURN: | ||
| self._log.info('received out of turn block', block=block.hash_hex, signer_id=block.signer_id) | ||
|
|
||
| self._schedule_block() | ||
|
|
||
| def _schedule_block(self) -> None: | ||
| """Schedule propagation of a new block.""" | ||
| if not self.manager.can_start_mining(): | ||
| # We're syncing, so we'll try again later | ||
| self._log.info('cannot produce new block, node not synced') | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel that the full node would not recover from getting out of sync after it was already synced. From recovery, I mean that this full node would stop producing blocks.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The full node recovers as long as it receives a block from another peer. I added an assertion in an existing test to cover this, in 0d4fea6 |
||
| return | ||
|
|
||
| if self._start_producing_lc.running: | ||
| self._start_producing_lc.stop() | ||
|
|
||
| previous_block = self.manager.tx_storage.get_best_block() | ||
| if not self._started_producing or previous_block == self._last_seen_best_block: | ||
| if previous_block == self._last_seen_best_block: | ||
| return | ||
|
|
||
| self._last_seen_best_block = previous_block | ||
|
|
@@ -139,7 +157,11 @@ def _schedule_block(self) -> None: | |
| expected_timestamp = self._expected_block_timestamp(previous_block, signer_index) | ||
| propagation_delay = 0 if expected_timestamp < now else expected_timestamp - now | ||
|
|
||
| if self._delayed_call and self._delayed_call.active(): | ||
| self._delayed_call.cancel() | ||
|
|
||
| self._delayed_call = self._reactor.callLater(propagation_delay, self._produce_block, previous_block) | ||
|
|
||
| self._log.debug( | ||
| 'scheduling block production', | ||
| previous_block=previous_block.hash_hex, | ||
|
|
@@ -153,30 +175,35 @@ def _produce_block(self, previous_block: PoaBlock) -> None: | |
| block_templates = self.manager.get_block_templates(parent_block_hash=previous_block.hash) | ||
| block = block_templates.generate_mining_block(self.manager.rng, cls=PoaBlock) | ||
| assert isinstance(block, PoaBlock) | ||
|
|
||
| if block.get_height() <= self.manager.tx_storage.get_height_best_block(): | ||
| return | ||
|
|
||
| signer_index = self._get_signer_index(previous_block) | ||
| block.weight = poa.calculate_weight(self._poa_settings, block, not_none(signer_index)) | ||
| self._poa_signer.sign_block(block) | ||
| block.update_hash() | ||
|
|
||
| self.manager.on_new_tx(block, propagate_to_peers=False, fails_silently=False) | ||
| if not block.get_metadata().voided_by: | ||
| self.manager.connections.send_tx_to_peers(block) | ||
|
|
||
| self._log.debug( | ||
| self._log.info( | ||
| 'produced new block', | ||
| block=block.hash_hex, | ||
| height=block.get_height(), | ||
| weight=block.weight, | ||
| parent=block.get_block_parent_hash().hex(), | ||
| voided=bool(block.get_metadata().voided_by), | ||
| ) | ||
| self.manager.on_new_tx(block, propagate_to_peers=True, fails_silently=False) | ||
|
|
||
| def _expected_block_timestamp(self, previous_block: Block, signer_index: int) -> int: | ||
| """Calculate the expected timestamp for a new block.""" | ||
| height = previous_block.get_height() + 1 | ||
| expected_index = poa.in_turn_signer_index(settings=self._poa_settings, height=height) | ||
| signers = poa.get_active_signers(self._poa_settings, height) | ||
| index_distance = (signer_index - expected_index) % len(signers) | ||
| assert 0 <= index_distance < len(signers) | ||
| index_distance = poa.get_signer_index_distance( | ||
| settings=self._poa_settings, | ||
| signer_index=signer_index, | ||
| height=height, | ||
| ) | ||
| delay = _SIGNER_TURN_INTERVAL * index_distance | ||
| if index_distance > 0: | ||
| # if it's not our turn, we add a constant offset to the delay | ||
| delay += self._settings.AVG_TIME_BETWEEN_BLOCKS | ||
| return previous_block.timestamp + self._settings.AVG_TIME_BETWEEN_BLOCKS + delay | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don't understand why we need this
LoopingCall. Anyway, I'm ok to get this PR merged and get this changed later because it has no impact on mainnet and testnet.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using either a
LoopingCallor a recursivecallLater, separate from the maincallLaterlogic (triggered reactively via pubsub) were the only good ways I found to implement this while covering the following requirements:can_start_mining()may throw an exception.I created an issue to review this later: #1097