Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions hathor/consensus/poa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
ValidSignature,
calculate_weight,
get_hashed_poa_data,
is_in_turn,
in_turn_signer_index,
verify_poa_signature,
)
from .poa_block_producer import PoaBlockProducer
Expand All @@ -17,7 +17,7 @@
'BLOCK_WEIGHT_OUT_OF_TURN',
'SIGNER_ID_LEN',
'get_hashed_poa_data',
'is_in_turn',
'in_turn_signer_index',
'calculate_weight',
'PoaBlockProducer',
'PoaSigner',
Expand Down
10 changes: 5 additions & 5 deletions hathor/consensus/poa/poa.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ def get_hashed_poa_data(block: PoaBlock) -> bytes:
return hashed_poa_data


def is_in_turn(*, settings: PoaSettings, height: int, signer_index: int) -> bool:
"""Return whether the given signer is in turn for the given height."""
return height % len(settings.signers) == signer_index
def in_turn_signer_index(settings: PoaSettings, height: int) -> int:
"""Return the signer index that is in turn for the given height."""
return height % len(settings.signers)


def calculate_weight(settings: PoaSettings, block: PoaBlock, signer_index: int) -> float:
"""Return the weight for the given block and signer."""
is_in_turn_flag = is_in_turn(settings=settings, height=block.get_height(), signer_index=signer_index)
return BLOCK_WEIGHT_IN_TURN if is_in_turn_flag else BLOCK_WEIGHT_OUT_OF_TURN
expected_index = in_turn_signer_index(settings, block.get_height())
return BLOCK_WEIGHT_IN_TURN if expected_index == signer_index else BLOCK_WEIGHT_OUT_OF_TURN


@dataclass(frozen=True, slots=True)
Expand Down
18 changes: 7 additions & 11 deletions hathor/consensus/poa/poa_block_producer.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
# Number of seconds to wait for a sync to finish before trying to produce blocks
_WAIT_SYNC_DELAY: int = 30

# Number of seconds used in random delay calculation
_RANDOM_DELAY_MULTIPLIER: int = 1
# Number of seconds used between each signer depending on its distance to the expected signer
_SIGNER_TURN_INTERVAL: int = 1


class PoaBlockProducer:
Expand Down Expand Up @@ -164,12 +164,8 @@ def _produce_block(self, previous_block: PoaBlock) -> None:
def _expected_block_timestamp(self, previous_block: Block) -> int:
"""Calculate the expected timestamp for a new block."""
height = previous_block.get_height() + 1
is_in_turn = poa.is_in_turn(settings=self._poa_settings, height=height, signer_index=self._signer_index)
timestamp = previous_block.timestamp + self._settings.AVG_TIME_BETWEEN_BLOCKS
if is_in_turn:
return timestamp

signer_count = len(self._poa_settings.signers)
assert signer_count >= 1
random_offset = self.manager.rng.choice(range(signer_count * _RANDOM_DELAY_MULTIPLIER)) + 1
return timestamp + random_offset
expected_index = poa.in_turn_signer_index(settings=self._poa_settings, height=height)
index_distance = (self._signer_index - expected_index) % len(self._poa_settings.signers)
assert 0 <= index_distance < len(self._poa_settings.signers)
delay = _SIGNER_TURN_INTERVAL * index_distance
return previous_block.timestamp + self._settings.AVG_TIME_BETWEEN_BLOCKS + delay
4 changes: 2 additions & 2 deletions tests/poa/test_poa.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,10 @@ def get_signer() -> tuple[PoaSigner, bytes]:
(2, 3, 1, True),
]
)
def test_is_in_turn(n_signers: int, height: int, signer_index: int, expected: bool) -> None:
def test_in_turn_signer_index(n_signers: int, height: int, signer_index: int, expected: bool) -> None:
settings = PoaSettings.construct(signers=tuple(b'' for _ in range(n_signers)))

result = poa.is_in_turn(settings=settings, height=height, signer_index=signer_index)
result = poa.in_turn_signer_index(settings=settings, height=height) == signer_index
assert result == expected


Expand Down
42 changes: 39 additions & 3 deletions tests/poa/test_poa_block_producer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@

from unittest.mock import Mock

import pytest

from hathor.conf.settings import HathorSettings
from hathor.consensus import poa
from hathor.consensus.consensus_settings import PoaSettings
from hathor.consensus.poa import PoaBlockProducer
from hathor.crypto.util import get_public_key_bytes_compressed
from hathor.manager import HathorManager
Expand Down Expand Up @@ -145,13 +148,46 @@ def test_poa_block_producer_two_signers() -> None:
reactor.advance(9)

# we produce our third block
reactor.advance(1 + 2) # TODO: We have to consider the random factor, but it'll be removed.
reactor.advance(2)
manager.on_new_tx.assert_called_once()
block3 = manager.on_new_tx.call_args.args[0]
assert isinstance(block3, PoaBlock)
# TODO: We have to consider the random factor, but it'll be removed.
assert block3.timestamp in (block2.timestamp + 10, block2.timestamp + 11, block2.timestamp + 12)
assert block3.timestamp == block2.timestamp + 11
assert block3.weight == poa.BLOCK_WEIGHT_OUT_OF_TURN
assert block3.outputs == []
assert block3.get_block_parent_hash() == block2.hash
manager.on_new_tx.reset_mock()


@pytest.mark.parametrize(
['previous_height', 'signer_index', 'expected_delay'],
[
(0, 0, 33),
(0, 1, 30),
(0, 2, 31),
(0, 3, 32),

(1, 0, 32),
(1, 1, 33),
(1, 2, 30),
(1, 3, 31),
]
)
def test_expected_block_timestamp(previous_height: int, signer_index: int, expected_delay: int) -> None:
signers = [get_signer(), get_signer(), get_signer(), get_signer()]
keys_and_signers = sorted(
(get_public_key_bytes_compressed(signer.get_public_key()), signer)
for signer in signers
)
signer = keys_and_signers[signer_index][1]
settings = Mock()
settings.CONSENSUS_ALGORITHM = PoaSettings(signers=tuple([x[0] for x in keys_and_signers]))
settings.AVG_TIME_BETWEEN_BLOCKS = 30
producer = PoaBlockProducer(settings=settings, reactor=Mock(), poa_signer=signer)
previous_block = Mock()
previous_block.timestamp = 100
previous_block.get_height = Mock(return_value=previous_height)

result = producer._expected_block_timestamp(previous_block)

assert result == previous_block.timestamp + expected_delay