Skip to content

Commit 53fa539

Browse files
authored
feat(side-dag): add signers validation on peer hello (#1078)
1 parent b98b938 commit 53fa539

20 files changed

+152
-90
lines changed

hathor/builder/builder.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
from typing_extensions import assert_never
2020

2121
from hathor.checkpoint import Checkpoint
22-
from hathor.conf.get_settings import get_global_settings
2322
from hathor.conf.settings import HathorSettings as HathorSettingsType
2423
from hathor.consensus import ConsensusAlgorithm
2524
from hathor.consensus.poa import PoaBlockProducer, PoaSigner
@@ -346,7 +345,7 @@ def set_peer_id(self, peer_id: PeerId) -> 'Builder':
346345
def _get_or_create_settings(self) -> HathorSettingsType:
347346
"""Return the HathorSettings instance set on this builder, or a new one if not set."""
348347
if self._settings is None:
349-
self._settings = get_global_settings()
348+
raise ValueError('settings not set')
350349
return self._settings
351350

352351
def _get_reactor(self) -> Reactor:
@@ -422,7 +421,8 @@ def _get_or_create_p2p_manager(self) -> ConnectionsManager:
422421
assert self._network is not None
423422

424423
self._p2p_manager = ConnectionsManager(
425-
reactor,
424+
settings=self._get_or_create_settings(),
425+
reactor=reactor,
426426
network=self._network,
427427
my_peer=my_peer,
428428
pubsub=self._get_or_create_pubsub(),

hathor/builder/cli_builder.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,8 @@ def create_manager(self, reactor: Reactor) -> HathorManager:
295295
cpu_mining_service = CpuMiningService()
296296

297297
p2p_manager = ConnectionsManager(
298-
reactor,
298+
settings=settings,
299+
reactor=reactor,
299300
network=network,
300301
my_peer=peer_id,
301302
pubsub=pubsub,

hathor/consensus/consensus_settings.py

+32-1
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import hashlib
1516
from abc import ABC, abstractmethod
1617
from enum import Enum, unique
1718
from typing import Annotated, Any, Literal, TypeAlias
1819

19-
from pydantic import Field, NonNegativeInt, validator
20+
from pydantic import Field, NonNegativeInt, PrivateAttr, validator
2021
from typing_extensions import override
2122

2223
from hathor.transaction import TxVersion
24+
from hathor.util import json_dumpb
2325
from hathor.utils.pydantic import BaseModel
2426

2527

@@ -31,6 +33,7 @@ class ConsensusType(str, Enum):
3133

3234
class _BaseConsensusSettings(ABC, BaseModel):
3335
type: ConsensusType
36+
_peer_hello_hash: str | None = PrivateAttr(default=None)
3437

3538
def is_pow(self) -> bool:
3639
"""Return whether this is a Proof-of-Work consensus."""
@@ -49,6 +52,16 @@ def is_vertex_version_valid(self, version: TxVersion, include_genesis: bool = Fa
4952
"""Return whether a `TxVersion` is valid for this consensus type."""
5053
return version in self._get_valid_vertex_versions(include_genesis)
5154

55+
def get_peer_hello_hash(self) -> str | None:
56+
"""Return a hash of consensus settings to be used in peer hello validation."""
57+
if self._peer_hello_hash is None:
58+
self._peer_hello_hash = self._calculate_peer_hello_hash()
59+
return self._peer_hello_hash
60+
61+
def _calculate_peer_hello_hash(self) -> str | None:
62+
"""Calculate a hash of consensus settings to be used in peer hello validation."""
63+
return None
64+
5265

5366
class PowSettings(_BaseConsensusSettings):
5467
type: Literal[ConsensusType.PROOF_OF_WORK] = ConsensusType.PROOF_OF_WORK
@@ -62,6 +75,10 @@ def _get_valid_vertex_versions(self, include_genesis: bool) -> set[TxVersion]:
6275
TxVersion.MERGE_MINED_BLOCK
6376
}
6477

78+
@override
79+
def get_peer_hello_hash(self) -> str | None:
80+
return None
81+
6582

6683
class PoaSignerSettings(BaseModel):
6784
public_key: bytes
@@ -86,6 +103,13 @@ def _validate_end_height(cls, end_height: int | None, values: dict[str, Any]) ->
86103

87104
return end_height
88105

106+
def to_json_dict(self) -> dict[str, Any]:
107+
"""Return this signer settings instance as a json dict."""
108+
json_dict = self.dict()
109+
# TODO: We can use a custom serializer to convert bytes to hex when we update to Pydantic V2.
110+
json_dict['public_key'] = self.public_key.hex()
111+
return json_dict
112+
89113

90114
class PoaSettings(_BaseConsensusSettings):
91115
type: Literal[ConsensusType.PROOF_OF_AUTHORITY] = ConsensusType.PROOF_OF_AUTHORITY
@@ -114,5 +138,12 @@ def _get_valid_vertex_versions(self, include_genesis: bool) -> set[TxVersion]:
114138

115139
return versions
116140

141+
@override
142+
def _calculate_peer_hello_hash(self) -> str | None:
143+
data = b''
144+
for signer in self.signers:
145+
data += json_dumpb(signer.to_json_dict())
146+
return hashlib.sha256(data).digest().hex()
147+
117148

118149
ConsensusSettings: TypeAlias = Annotated[PowSettings | PoaSettings, Field(discriminator='type')]

hathor/consensus/poa/poa.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,9 @@ def verify_poa_signature(settings: PoaSettings, block: PoaBlock) -> InvalidSigna
8282
"""Return whether the provided public key was used to sign the block Proof-of-Authority."""
8383
from hathor.consensus.poa import PoaSigner
8484
active_signers = get_active_signers(settings, block.get_height())
85-
sorted_signers = sorted(active_signers)
8685
hashed_poa_data = get_hashed_poa_data(block)
8786

88-
for signer_index, public_key_bytes in enumerate(sorted_signers):
87+
for signer_index, public_key_bytes in enumerate(active_signers):
8988
signer_id = PoaSigner.get_poa_signer_id(public_key_bytes)
9089
if block.signer_id != signer_id:
9190
# this is not our signer

hathor/consensus/poa/poa_block_producer.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,8 @@ def _get_signer_index(self, previous_block: Block) -> int | None:
108108
public_key = self._poa_signer.get_public_key()
109109
public_key_bytes = get_public_key_bytes_compressed(public_key)
110110
active_signers = poa.get_active_signers(self._poa_settings, height)
111-
sorted_signers = sorted(active_signers)
112111
try:
113-
return sorted_signers.index(public_key_bytes)
112+
return active_signers.index(public_key_bytes)
114113
except ValueError:
115114
return None
116115

hathor/p2p/factory.py

+19-12
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from twisted.internet import protocol
1818
from twisted.internet.interfaces import IAddress
1919

20+
from hathor.conf.settings import HathorSettings
2021
from hathor.p2p.manager import ConnectionsManager
2122
from hathor.p2p.peer_id import PeerId
2223
from hathor.p2p.protocol import HathorLineReceiver
@@ -36,14 +37,16 @@ class HathorServerFactory(protocol.ServerFactory):
3637
protocol: type[MyServerProtocol] = MyServerProtocol
3738

3839
def __init__(
39-
self,
40-
network: str,
41-
my_peer: PeerId,
42-
p2p_manager: ConnectionsManager,
43-
*,
44-
use_ssl: bool,
40+
self,
41+
network: str,
42+
my_peer: PeerId,
43+
p2p_manager: ConnectionsManager,
44+
*,
45+
settings: HathorSettings,
46+
use_ssl: bool,
4547
):
4648
super().__init__()
49+
self._settings = settings
4750
self.network = network
4851
self.my_peer = my_peer
4952
self.p2p_manager = p2p_manager
@@ -57,6 +60,7 @@ def buildProtocol(self, addr: IAddress) -> MyServerProtocol:
5760
p2p_manager=self.p2p_manager,
5861
use_ssl=self.use_ssl,
5962
inbound=True,
63+
settings=self._settings
6064
)
6165
p.factory = self
6266
return p
@@ -69,14 +73,16 @@ class HathorClientFactory(protocol.ClientFactory):
6973
protocol: type[MyClientProtocol] = MyClientProtocol
7074

7175
def __init__(
72-
self,
73-
network: str,
74-
my_peer: PeerId,
75-
p2p_manager: ConnectionsManager,
76-
*,
77-
use_ssl: bool,
76+
self,
77+
network: str,
78+
my_peer: PeerId,
79+
p2p_manager: ConnectionsManager,
80+
*,
81+
settings: HathorSettings,
82+
use_ssl: bool,
7883
):
7984
super().__init__()
85+
self._settings = settings
8086
self.network = network
8187
self.my_peer = my_peer
8288
self.p2p_manager = p2p_manager
@@ -90,6 +96,7 @@ def buildProtocol(self, addr: IAddress) -> MyClientProtocol:
9096
p2p_manager=self.p2p_manager,
9197
use_ssl=self.use_ssl,
9298
inbound=False,
99+
settings=self._settings
93100
)
94101
p.factory = self
95102
return p

hathor/p2p/manager.py

+23-16
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from twisted.python.failure import Failure
2525
from twisted.web.client import Agent
2626

27-
from hathor.conf.get_settings import get_global_settings
27+
from hathor.conf.settings import HathorSettings
2828
from hathor.p2p.entrypoint import Entrypoint
2929
from hathor.p2p.netfilter.factory import NetfilterFactory
3030
from hathor.p2p.peer_discovery import PeerDiscovery
@@ -45,7 +45,6 @@
4545
from hathor.manager import HathorManager
4646

4747
logger = get_logger()
48-
settings = get_global_settings()
4948

5049
# The timeout in seconds for the whitelist GET request
5150
WHITELIST_REQUEST_TIMEOUT = 45
@@ -74,9 +73,6 @@ class PeerConnectionsMetrics(NamedTuple):
7473
class ConnectionsManager:
7574
""" It manages all peer-to-peer connections and events related to control messages.
7675
"""
77-
MAX_ENABLED_SYNC = settings.MAX_ENABLED_SYNC
78-
SYNC_UPDATE_INTERVAL = settings.SYNC_UPDATE_INTERVAL
79-
PEER_DISCOVERY_INTERVAL = settings.PEER_DISCOVERY_INTERVAL
8076

8177
class GlobalRateLimiter:
8278
SEND_TIPS = 'NodeSyncTimestamp.send_tips'
@@ -92,18 +88,25 @@ class GlobalRateLimiter:
9288

9389
rate_limiter: RateLimiter
9490

95-
def __init__(self,
96-
reactor: Reactor,
97-
network: str,
98-
my_peer: PeerId,
99-
pubsub: PubSubManager,
100-
ssl: bool,
101-
rng: Random,
102-
whitelist_only: bool) -> None:
91+
def __init__(
92+
self,
93+
settings: HathorSettings,
94+
reactor: Reactor,
95+
network: str,
96+
my_peer: PeerId,
97+
pubsub: PubSubManager,
98+
ssl: bool,
99+
rng: Random,
100+
whitelist_only: bool,
101+
) -> None:
103102
self.log = logger.new()
103+
self._settings = settings
104104
self.rng = rng
105105
self.manager = None
106-
self._settings = get_global_settings()
106+
107+
self.MAX_ENABLED_SYNC = settings.MAX_ENABLED_SYNC
108+
self.SYNC_UPDATE_INTERVAL = settings.SYNC_UPDATE_INTERVAL
109+
self.PEER_DISCOVERY_INTERVAL = settings.PEER_DISCOVERY_INTERVAL
107110

108111
self.reactor = reactor
109112
self.my_peer = my_peer
@@ -125,8 +128,12 @@ def __init__(self,
125128
# Factories.
126129
from hathor.p2p.factory import HathorClientFactory, HathorServerFactory
127130
self.use_ssl = ssl
128-
self.server_factory = HathorServerFactory(self.network, self.my_peer, p2p_manager=self, use_ssl=self.use_ssl)
129-
self.client_factory = HathorClientFactory(self.network, self.my_peer, p2p_manager=self, use_ssl=self.use_ssl)
131+
self.server_factory = HathorServerFactory(
132+
self.network, self.my_peer, p2p_manager=self, use_ssl=self.use_ssl, settings=self._settings
133+
)
134+
self.client_factory = HathorClientFactory(
135+
self.network, self.my_peer, p2p_manager=self, use_ssl=self.use_ssl, settings=self._settings
136+
)
130137

131138
# Global maximum number of connections.
132139
self.max_connections: int = self._settings.PEER_MAX_CONNECTIONS

hathor/p2p/protocol.py

+13-5
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from twisted.protocols.basic import LineReceiver
2424
from twisted.python.failure import Failure
2525

26-
from hathor.conf.get_settings import get_global_settings
26+
from hathor.conf.settings import HathorSettings
2727
from hathor.p2p.entrypoint import Entrypoint
2828
from hathor.p2p.messages import ProtocolMessages
2929
from hathor.p2p.peer_id import PeerId
@@ -91,9 +91,17 @@ class WarningFlags(str, Enum):
9191
sync_version: Optional[SyncVersion] # version chosen to be used on this connection
9292
capabilities: set[str] # capabilities received from the peer in HelloState
9393

94-
def __init__(self, network: str, my_peer: PeerId, p2p_manager: 'ConnectionsManager',
95-
*, use_ssl: bool, inbound: bool) -> None:
96-
self._settings = get_global_settings()
94+
def __init__(
95+
self,
96+
network: str,
97+
my_peer: PeerId,
98+
p2p_manager: 'ConnectionsManager',
99+
*,
100+
settings: HathorSettings,
101+
use_ssl: bool,
102+
inbound: bool,
103+
) -> None:
104+
self._settings = settings
97105
self.network = network
98106
self.my_peer = my_peer
99107
self.connections = p2p_manager
@@ -164,7 +172,7 @@ def change_state(self, state_enum: PeerState) -> None:
164172
"""Called to change the state of the connection."""
165173
if state_enum not in self._state_instances:
166174
state_cls = state_enum.value
167-
instance = state_cls(self)
175+
instance = state_cls(self, self._settings)
168176
instance.state_name = state_enum.name
169177
self._state_instances[state_enum] = instance
170178
new_state = self._state_instances[state_enum]

hathor/p2p/states/base.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from structlog import get_logger
1919
from twisted.internet.defer import Deferred
2020

21+
from hathor.conf.settings import HathorSettings
2122
from hathor.p2p.messages import ProtocolMessages
2223

2324
if TYPE_CHECKING:
@@ -33,8 +34,9 @@ class BaseState:
3334
Callable[[str], None] | Callable[[str], Deferred[None]] | Callable[[str], Coroutine[Deferred[None], Any, None]]
3435
]
3536

36-
def __init__(self, protocol: 'HathorProtocol'):
37+
def __init__(self, protocol: 'HathorProtocol', settings: HathorSettings):
3738
self.log = logger.new(**protocol.get_logger_context())
39+
self._settings = settings
3840
self.protocol = protocol
3941
self.cmd_map = {
4042
ProtocolMessages.ERROR: self.handle_error,

hathor/p2p/states/hello.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import hathor
2020
from hathor.conf.get_settings import get_global_settings
21+
from hathor.conf.settings import HathorSettings
2122
from hathor.exception import HathorError
2223
from hathor.p2p.messages import ProtocolMessages
2324
from hathor.p2p.states.base import BaseState
@@ -32,9 +33,8 @@
3233

3334

3435
class HelloState(BaseState):
35-
def __init__(self, protocol: 'HathorProtocol') -> None:
36-
super().__init__(protocol)
37-
self._settings = get_global_settings()
36+
def __init__(self, protocol: 'HathorProtocol', settings: HathorSettings) -> None:
37+
super().__init__(protocol, settings)
3838
self.log = logger.new(**protocol.get_logger_context())
3939
self.cmd_map.update({
4040
ProtocolMessages.HELLO: self.handle_hello,
@@ -56,7 +56,7 @@ def _get_hello_data(self) -> dict[str, Any]:
5656
'remote_address': format_address(remote),
5757
'genesis_short_hash': get_genesis_short_hash(),
5858
'timestamp': protocol.node.reactor.seconds(),
59-
'settings_dict': get_settings_hello_dict(),
59+
'settings_dict': get_settings_hello_dict(self._settings),
6060
'capabilities': protocol.node.capabilities,
6161
}
6262
if self.protocol.node.has_sync_version_capability():
@@ -150,7 +150,7 @@ def handle_hello(self, payload: str) -> None:
150150

151151
if 'settings_dict' in data:
152152
# If settings_dict is sent we must validate it
153-
settings_dict = get_settings_hello_dict()
153+
settings_dict = get_settings_hello_dict(self._settings)
154154
if data['settings_dict'] != settings_dict:
155155
protocol.send_error_and_close_connection(
156156
'Settings values are different. {}'.format(json_dumps(settings_dict))

0 commit comments

Comments
 (0)