Skip to content

Commit c4e2dea

Browse files
committed
refactor(side-dag): general improvements
1 parent 5e46ebd commit c4e2dea

File tree

6 files changed

+141
-81
lines changed

6 files changed

+141
-81
lines changed

hathor/consensus/poa/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
calculate_weight,
88
get_active_signers,
99
get_hashed_poa_data,
10-
in_turn_signer_index,
10+
get_signer_index_distance,
1111
verify_poa_signature,
1212
)
1313
from .poa_block_producer import PoaBlockProducer
@@ -18,7 +18,6 @@
1818
'BLOCK_WEIGHT_OUT_OF_TURN',
1919
'SIGNER_ID_LEN',
2020
'get_hashed_poa_data',
21-
'in_turn_signer_index',
2221
'calculate_weight',
2322
'PoaBlockProducer',
2423
'PoaSigner',
@@ -27,4 +26,5 @@
2726
'InvalidSignature',
2827
'ValidSignature',
2928
'get_active_signers',
29+
'get_signer_index_distance',
3030
]

hathor/consensus/poa/poa.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,21 @@ def get_active_signers(settings: PoaSettings, height: int) -> list[bytes]:
5555
return active_signers
5656

5757

58-
def in_turn_signer_index(settings: PoaSettings, height: int) -> int:
59-
"""Return the signer index that is in turn for the given height."""
58+
def get_signer_index_distance(*, settings: PoaSettings, signer_index: int, height: int) -> int:
59+
"""Considering a block height, return the signer index distance to that block. When the distance is 0, it means it
60+
is the signer's turn."""
6061
active_signers = get_active_signers(settings, height)
61-
return height % len(active_signers)
62+
expected_index = height % len(active_signers)
63+
signers = get_active_signers(settings, height)
64+
index_distance = (signer_index - expected_index) % len(signers)
65+
assert 0 <= index_distance < len(signers)
66+
return index_distance
6267

6368

6469
def calculate_weight(settings: PoaSettings, block: PoaBlock, signer_index: int) -> float:
6570
"""Return the weight for the given block and signer."""
66-
expected_index = in_turn_signer_index(settings, block.get_height())
67-
return BLOCK_WEIGHT_IN_TURN if expected_index == signer_index else BLOCK_WEIGHT_OUT_OF_TURN
71+
index_distance = get_signer_index_distance(settings=settings, signer_index=signer_index, height=block.get_height())
72+
return BLOCK_WEIGHT_IN_TURN if index_distance == 0 else BLOCK_WEIGHT_OUT_OF_TURN / index_distance
6873

6974

7075
@dataclass(frozen=True, slots=True)

hathor/consensus/poa/poa_block_producer.py

+34-33
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from typing import TYPE_CHECKING
1818

1919
from structlog import get_logger
20+
from twisted.internet.base import DelayedCall
2021
from twisted.internet.interfaces import IDelayedCall
2122
from twisted.internet.task import LoopingCall
2223

@@ -35,11 +36,8 @@
3536

3637
logger = get_logger()
3738

38-
# Number of seconds to wait for a sync to finish before trying to produce blocks
39-
_WAIT_SYNC_DELAY: int = 30
40-
4139
# Number of seconds used between each signer depending on its distance to the expected signer
42-
_SIGNER_TURN_INTERVAL: int = 1
40+
_SIGNER_TURN_INTERVAL: int = 10
4341

4442

4543
class PoaBlockProducer:
@@ -54,8 +52,6 @@ class PoaBlockProducer:
5452
'_reactor',
5553
'_manager',
5654
'_poa_signer',
57-
'_started_producing',
58-
'_start_producing_lc',
5955
'_schedule_block_lc',
6056
'_last_seen_best_block',
6157
'_delayed_call',
@@ -71,10 +67,6 @@ def __init__(self, *, settings: HathorSettings, reactor: ReactorProtocol, poa_si
7167
self._poa_signer = poa_signer
7268
self._last_seen_best_block: Block | None = None
7369

74-
self._started_producing = False
75-
self._start_producing_lc = LoopingCall(self._start_producing)
76-
self._start_producing_lc.clock = self._reactor
77-
7870
self._schedule_block_lc = LoopingCall(self._schedule_block)
7971
self._schedule_block_lc.clock = self._reactor
8072
self._delayed_call: IDelayedCall | None = None
@@ -89,13 +81,9 @@ def manager(self, manager: HathorManager) -> None:
8981
self._manager = manager
9082

9183
def start(self) -> None:
92-
self._start_producing_lc.start(_WAIT_SYNC_DELAY)
9384
self._schedule_block_lc.start(self._settings.AVG_TIME_BETWEEN_BLOCKS)
9485

9586
def stop(self) -> None:
96-
if self._start_producing_lc.running:
97-
self._start_producing_lc.stop()
98-
9987
if self._schedule_block_lc.running:
10088
self._schedule_block_lc.stop()
10189

@@ -113,21 +101,21 @@ def _get_signer_index(self, previous_block: Block) -> int | None:
113101
except ValueError:
114102
return None
115103

116-
def _start_producing(self) -> None:
117-
"""Start producing new blocks."""
104+
def _schedule_block(self) -> None:
105+
try:
106+
self._unsafe_schedule_block()
107+
except Exception:
108+
self._log.exception('error while scheduling block')
109+
110+
def _unsafe_schedule_block(self) -> None:
111+
"""Schedule propagation of a new block."""
118112
if not self.manager.can_start_mining():
119113
# We're syncing, so we'll try again later
120-
self._log.warn('cannot start producing new blocks, node not synced')
114+
self._log.info('cannot produce new block, node not synced')
121115
return
122116

123-
self._log.info('started producing new blocks')
124-
self._started_producing = True
125-
self._start_producing_lc.stop()
126-
127-
def _schedule_block(self) -> None:
128-
"""Schedule propagation of a new block."""
129117
previous_block = self.manager.tx_storage.get_best_block()
130-
if not self._started_producing or previous_block == self._last_seen_best_block:
118+
if previous_block == self._last_seen_best_block:
131119
return
132120

133121
self._last_seen_best_block = previous_block
@@ -139,6 +127,15 @@ def _schedule_block(self) -> None:
139127
expected_timestamp = self._expected_block_timestamp(previous_block, signer_index)
140128
propagation_delay = 0 if expected_timestamp < now else expected_timestamp - now
141129

130+
if self._delayed_call and self._delayed_call.active():
131+
from hathor.transaction.poa import PoaBlock
132+
assert isinstance(self._delayed_call, DelayedCall)
133+
delayed_block = self._delayed_call.args[0]
134+
assert isinstance(delayed_block, PoaBlock)
135+
if delayed_block.weight != poa.BLOCK_WEIGHT_IN_TURN:
136+
# we only cancel our delayed block if it was out of turn
137+
self._delayed_call.cancel()
138+
142139
self._delayed_call = self._reactor.callLater(propagation_delay, self._produce_block, previous_block)
143140
self._log.debug(
144141
'scheduling block production',
@@ -158,25 +155,29 @@ def _produce_block(self, previous_block: PoaBlock) -> None:
158155
self._poa_signer.sign_block(block)
159156
block.update_hash()
160157

161-
self.manager.on_new_tx(block, propagate_to_peers=False, fails_silently=False)
162-
if not block.get_metadata().voided_by:
163-
self.manager.connections.send_tx_to_peers(block)
164-
165-
self._log.debug(
158+
self._log.info(
166159
'produced new block',
167160
block=block.hash_hex,
168161
height=block.get_height(),
169162
weight=block.weight,
170163
parent=block.get_block_parent_hash().hex(),
171164
voided=bool(block.get_metadata().voided_by),
172165
)
166+
self.manager.on_new_tx(block, propagate_to_peers=False, fails_silently=False)
167+
if not block.get_metadata().voided_by:
168+
self.manager.connections.send_tx_to_peers(block)
169+
self._delayed_call = None
173170

174171
def _expected_block_timestamp(self, previous_block: Block, signer_index: int) -> int:
175172
"""Calculate the expected timestamp for a new block."""
176173
height = previous_block.get_height() + 1
177-
expected_index = poa.in_turn_signer_index(settings=self._poa_settings, height=height)
178-
signers = poa.get_active_signers(self._poa_settings, height)
179-
index_distance = (signer_index - expected_index) % len(signers)
180-
assert 0 <= index_distance < len(signers)
174+
index_distance = poa.get_signer_index_distance(
175+
settings=self._poa_settings,
176+
signer_index=signer_index,
177+
height=height,
178+
)
181179
delay = _SIGNER_TURN_INTERVAL * index_distance
180+
if index_distance > 0:
181+
# if it's not our turn, we add a constant offset to the delay
182+
delay += self._settings.AVG_TIME_BETWEEN_BLOCKS
182183
return previous_block.timestamp + self._settings.AVG_TIME_BETWEEN_BLOCKS + delay

tests/poa/test_poa.py

+28-13
Original file line numberDiff line numberDiff line change
@@ -218,40 +218,55 @@ def get_signer() -> tuple[PoaSigner, bytes]:
218218
@pytest.mark.parametrize(
219219
['n_signers', 'height', 'signer_index', 'expected'],
220220
[
221-
(1, 1, 0, True),
222-
(1, 2, 0, True),
223-
(1, 3, 0, True),
224-
225-
(2, 1, 0, False),
226-
(2, 2, 0, True),
227-
(2, 3, 0, False),
228-
229-
(2, 1, 1, True),
230-
(2, 2, 1, False),
231-
(2, 3, 1, True),
221+
(1, 1, 0, 0),
222+
(1, 2, 0, 0),
223+
(1, 3, 0, 0),
224+
225+
(2, 1, 0, 1),
226+
(2, 2, 0, 0),
227+
(2, 3, 0, 1),
228+
229+
(2, 1, 1, 0),
230+
(2, 2, 1, 1),
231+
(2, 3, 1, 0),
232+
233+
(5, 1, 0, 4),
234+
(5, 2, 0, 3),
235+
(5, 3, 0, 2),
236+
(5, 4, 0, 1),
237+
(5, 5, 0, 0),
232238
]
233239
)
234-
def test_in_turn_signer_index(n_signers: int, height: int, signer_index: int, expected: bool) -> None:
240+
def test_get_signer_index_distance(n_signers: int, height: int, signer_index: int, expected: int) -> None:
235241
settings = PoaSettings.construct(signers=tuple(PoaSignerSettings(public_key=b'') for _ in range(n_signers)))
236242

237-
result = poa.in_turn_signer_index(settings=settings, height=height) == signer_index
243+
result = poa.get_signer_index_distance(settings=settings, signer_index=signer_index, height=height)
238244
assert result == expected
239245

240246

241247
@pytest.mark.parametrize(
242248
['n_signers', 'height', 'signer_index', 'expected'],
243249
[
250+
(1, 0, 0, poa.BLOCK_WEIGHT_IN_TURN),
244251
(1, 1, 0, poa.BLOCK_WEIGHT_IN_TURN),
245252
(1, 2, 0, poa.BLOCK_WEIGHT_IN_TURN),
246253
(1, 3, 0, poa.BLOCK_WEIGHT_IN_TURN),
247254
255+
(2, 0, 0, poa.BLOCK_WEIGHT_IN_TURN),
248256
(2, 1, 0, poa.BLOCK_WEIGHT_OUT_OF_TURN),
249257
(2, 2, 0, poa.BLOCK_WEIGHT_IN_TURN),
250258
(2, 3, 0, poa.BLOCK_WEIGHT_OUT_OF_TURN),
251259
260+
(2, 0, 1, poa.BLOCK_WEIGHT_OUT_OF_TURN),
252261
(2, 1, 1, poa.BLOCK_WEIGHT_IN_TURN),
253262
(2, 2, 1, poa.BLOCK_WEIGHT_OUT_OF_TURN),
254263
(2, 3, 1, poa.BLOCK_WEIGHT_IN_TURN),
264+
265+
(5, 0, 0, poa.BLOCK_WEIGHT_IN_TURN),
266+
(5, 1, 0, poa.BLOCK_WEIGHT_OUT_OF_TURN / 4),
267+
(5, 2, 0, poa.BLOCK_WEIGHT_OUT_OF_TURN / 3),
268+
(5, 3, 0, poa.BLOCK_WEIGHT_OUT_OF_TURN / 2),
269+
(5, 4, 0, poa.BLOCK_WEIGHT_OUT_OF_TURN / 1),
255270
]
256271
)
257272
def test_calculate_weight(n_signers: int, height: int, signer_index: int, expected: float) -> None:

tests/poa/test_poa_block_producer.py

+9-11
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ def test_poa_block_producer_one_signer() -> None:
5757

5858
# when we can start mining, we start producing blocks
5959
manager.can_start_mining = Mock(return_value=True)
60-
reactor.advance(20)
6160

6261
# we produce our first block
6362
reactor.advance(10)
@@ -116,7 +115,6 @@ def test_poa_block_producer_two_signers() -> None:
116115

117116
# when we can start mining, we start producing blocks
118117
manager.can_start_mining = Mock(return_value=True)
119-
reactor.advance(20)
120118

121119
# we produce our first block
122120
reactor.advance(10)
@@ -144,14 +142,14 @@ def test_poa_block_producer_two_signers() -> None:
144142
manager.on_new_tx.reset_mock()
145143

146144
# haven't produced the third block yet
147-
reactor.advance(9)
145+
reactor.advance(29)
148146

149147
# we produce our third block
150-
reactor.advance(2)
148+
reactor.advance(1)
151149
manager.on_new_tx.assert_called_once()
152150
block3 = manager.on_new_tx.call_args.args[0]
153151
assert isinstance(block3, PoaBlock)
154-
assert block3.timestamp == block2.timestamp + 11
152+
assert block3.timestamp == block2.timestamp + 30
155153
assert block3.weight == poa.BLOCK_WEIGHT_OUT_OF_TURN
156154
assert block3.outputs == []
157155
assert block3.get_block_parent_hash() == block2.hash
@@ -161,15 +159,15 @@ def test_poa_block_producer_two_signers() -> None:
161159
@pytest.mark.parametrize(
162160
['previous_height', 'signer_index', 'expected_delay'],
163161
[
164-
(0, 0, 33),
162+
(0, 0, 90),
165163
(0, 1, 30),
166-
(0, 2, 31),
167-
(0, 3, 32),
164+
(0, 2, 70),
165+
(0, 3, 80),
168166
169-
(1, 0, 32),
170-
(1, 1, 33),
167+
(1, 0, 80),
168+
(1, 1, 90),
171169
(1, 2, 30),
172-
(1, 3, 31),
170+
(1, 3, 70),
173171
]
174172
)
175173
def test_expected_block_timestamp(previous_height: int, signer_index: int, expected_delay: int) -> None:

0 commit comments

Comments
 (0)