mech-client 0.14.1__py3-none-any.whl → 0.15.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mech_client/__init__.py +1 -1
- mech_client/cli.py +258 -11
- mech_client/configs/mechs.json +110 -110
- mech_client/interact.py +6 -1
- mech_client/marketplace_interact.py +160 -42
- mech_client/safe.py +73 -0
- mech_client/subgraph.py +0 -11
- {mech_client-0.14.1.dist-info → mech_client-0.15.1.dist-info}/METADATA +278 -224
- {mech_client-0.14.1.dist-info → mech_client-0.15.1.dist-info}/RECORD +22 -48
- scripts/deposit_native.py +48 -16
- scripts/deposit_token.py +107 -31
- scripts/nvm_subscribe.py +14 -6
- scripts/nvm_subscription/contracts/base_contract.py +9 -1
- scripts/nvm_subscription/contracts/nft_sales.py +1 -3
- scripts/nvm_subscription/contracts/subscription_provider.py +2 -4
- scripts/nvm_subscription/contracts/token.py +23 -5
- scripts/nvm_subscription/manager.py +109 -16
- scripts/utils.py +2 -2
- scripts/whitelist.py +5 -1
- mech_client/helpers/acn/README.md +0 -76
- mech_client/helpers/acn/__init__.py +0 -30
- mech_client/helpers/acn/acn.proto +0 -71
- mech_client/helpers/acn/acn_pb2.py +0 -42
- mech_client/helpers/acn/custom_types.py +0 -224
- mech_client/helpers/acn/dialogues.py +0 -126
- mech_client/helpers/acn/message.py +0 -274
- mech_client/helpers/acn/protocol.yaml +0 -24
- mech_client/helpers/acn/serialization.py +0 -149
- mech_client/helpers/acn/tests/__init__.py +0 -20
- mech_client/helpers/acn/tests/test_acn.py +0 -256
- mech_client/helpers/acn/tests/test_acn_dialogues.py +0 -53
- mech_client/helpers/acn/tests/test_acn_messages.py +0 -117
- mech_client/helpers/acn_data_share/README.md +0 -32
- mech_client/helpers/acn_data_share/__init__.py +0 -32
- mech_client/helpers/acn_data_share/acn_data_share.proto +0 -17
- mech_client/helpers/acn_data_share/acn_data_share_pb2.py +0 -29
- mech_client/helpers/acn_data_share/dialogues.py +0 -115
- mech_client/helpers/acn_data_share/message.py +0 -213
- mech_client/helpers/acn_data_share/protocol.yaml +0 -21
- mech_client/helpers/acn_data_share/serialization.py +0 -111
- mech_client/helpers/acn_data_share/tests/test_acn_data_share_dialogues.py +0 -49
- mech_client/helpers/acn_data_share/tests/test_acn_data_share_messages.py +0 -53
- mech_client/helpers/p2p_libp2p_client/README.md +0 -15
- mech_client/helpers/p2p_libp2p_client/__init__.py +0 -21
- mech_client/helpers/p2p_libp2p_client/connection.py +0 -703
- mech_client/helpers/p2p_libp2p_client/connection.yaml +0 -52
- {mech_client-0.14.1.dist-info → mech_client-0.15.1.dist-info}/LICENSE +0 -0
- {mech_client-0.14.1.dist-info → mech_client-0.15.1.dist-info}/WHEEL +0 -0
- {mech_client-0.14.1.dist-info → mech_client-0.15.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,703 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
# ------------------------------------------------------------------------------
|
|
3
|
-
#
|
|
4
|
-
# Copyright 2022-2024 Valory AG
|
|
5
|
-
# Copyright 2018-2019 Fetch.AI Limited
|
|
6
|
-
#
|
|
7
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
-
# you may not use this file except in compliance with the License.
|
|
9
|
-
# You may obtain a copy of the License at
|
|
10
|
-
#
|
|
11
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
-
#
|
|
13
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
-
# See the License for the specific language governing permissions and
|
|
17
|
-
# limitations under the License.
|
|
18
|
-
#
|
|
19
|
-
# ------------------------------------------------------------------------------
|
|
20
|
-
"""This module contains the libp2p client connection."""
|
|
21
|
-
# pylint: disable-all
|
|
22
|
-
import asyncio
|
|
23
|
-
import contextlib
|
|
24
|
-
import hashlib
|
|
25
|
-
import logging
|
|
26
|
-
import random
|
|
27
|
-
import ssl
|
|
28
|
-
from asyncio import CancelledError
|
|
29
|
-
from asyncio.events import AbstractEventLoop
|
|
30
|
-
from asyncio.streams import StreamWriter
|
|
31
|
-
from pathlib import Path
|
|
32
|
-
from typing import Any, Dict, List, Optional
|
|
33
|
-
|
|
34
|
-
from asn1crypto import x509 # type: ignore
|
|
35
|
-
from ecdsa.curves import SECP256k1 # type: ignore
|
|
36
|
-
from ecdsa.keys import BadSignatureError, VerifyingKey # type: ignore
|
|
37
|
-
from ecdsa.util import sigdecode_der # type: ignore
|
|
38
|
-
|
|
39
|
-
from aea.configurations.base import PublicId
|
|
40
|
-
from aea.configurations.constants import DEFAULT_LEDGER
|
|
41
|
-
from aea.connections.base import Connection, ConnectionStates
|
|
42
|
-
from aea.crypto.registries import make_crypto
|
|
43
|
-
from aea.exceptions import enforce
|
|
44
|
-
from aea.helpers.acn.agent_record import AgentRecord
|
|
45
|
-
from aea.helpers.acn.uri import Uri
|
|
46
|
-
from aea.helpers.pipe import IPCChannelClient, TCPSocketChannelClient, TCPSocketProtocol
|
|
47
|
-
from aea.mail.base import Envelope
|
|
48
|
-
|
|
49
|
-
from packages.valory.protocols.acn import acn_pb2
|
|
50
|
-
from packages.valory.protocols.acn.message import AcnMessage
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
try:
|
|
54
|
-
from asyncio.streams import ( # type: ignore # pylint: disable=ungrouped-imports
|
|
55
|
-
IncompleteReadError,
|
|
56
|
-
)
|
|
57
|
-
except ImportError: # pragma: nocover
|
|
58
|
-
from asyncio import IncompleteReadError # pylint: disable=ungrouped-imports
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
_default_logger = logging.getLogger("aea.packages.valory.connections.p2p_libp2p_client")
|
|
62
|
-
|
|
63
|
-
PUBLIC_ID = PublicId.from_str("valory/p2p_libp2p_client:0.1.0")
|
|
64
|
-
|
|
65
|
-
SUPPORTED_LEDGER_IDS = ["fetchai", "cosmos", "ethereum"]
|
|
66
|
-
|
|
67
|
-
POR_DEFAULT_SERVICE_ID = "acn"
|
|
68
|
-
|
|
69
|
-
ACN_CURRENT_VERSION = "0.1.0"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
class NodeClient:
|
|
73
|
-
"""Client to communicate with node using ipc channel(pipe)."""
|
|
74
|
-
|
|
75
|
-
ACN_ACK_TIMEOUT = 5.0
|
|
76
|
-
|
|
77
|
-
def __init__(self, pipe: IPCChannelClient, node_por: AgentRecord) -> None:
|
|
78
|
-
"""Set node client with pipe."""
|
|
79
|
-
self.pipe = pipe
|
|
80
|
-
self._wait_status: Optional[asyncio.Future] = None
|
|
81
|
-
self.agent_record = node_por
|
|
82
|
-
|
|
83
|
-
async def wait_for_status(self) -> Any:
|
|
84
|
-
"""Get status."""
|
|
85
|
-
if self._wait_status is None: # pragma: nocover
|
|
86
|
-
raise ValueError("waiter for status not set!")
|
|
87
|
-
return await asyncio.wait_for(self._wait_status, timeout=self.ACN_ACK_TIMEOUT)
|
|
88
|
-
|
|
89
|
-
@staticmethod
|
|
90
|
-
def make_acn_envelope_message(envelope: Envelope) -> bytes:
|
|
91
|
-
"""Make acn message with envelope in."""
|
|
92
|
-
acn_msg = acn_pb2.AcnMessage() # type: ignore
|
|
93
|
-
performative = acn_pb2.AcnMessage.Aea_Envelope_Performative() # type: ignore
|
|
94
|
-
performative.envelope = envelope.encode()
|
|
95
|
-
acn_msg.aea_envelope.CopyFrom(performative) # pylint: disable=no-member
|
|
96
|
-
buf = acn_msg.SerializeToString()
|
|
97
|
-
return buf
|
|
98
|
-
|
|
99
|
-
async def write_acn_status_ok(self) -> None:
|
|
100
|
-
"""Send acn status ok."""
|
|
101
|
-
acn_msg = acn_pb2.AcnMessage() # type: ignore
|
|
102
|
-
performative = acn_pb2.AcnMessage.Status_Performative() # type: ignore
|
|
103
|
-
status = AcnMessage.StatusBody(
|
|
104
|
-
status_code=AcnMessage.StatusBody.StatusCode.SUCCESS, msgs=[]
|
|
105
|
-
)
|
|
106
|
-
AcnMessage.StatusBody.encode(
|
|
107
|
-
performative.body, status # pylint: disable=no-member
|
|
108
|
-
)
|
|
109
|
-
acn_msg.status.CopyFrom(performative) # pylint: disable=no-member
|
|
110
|
-
buf = acn_msg.SerializeToString()
|
|
111
|
-
await self._write(buf)
|
|
112
|
-
|
|
113
|
-
async def write_acn_status_error(
|
|
114
|
-
self,
|
|
115
|
-
msg: str,
|
|
116
|
-
status_code: AcnMessage.StatusBody.StatusCode = AcnMessage.StatusBody.StatusCode.ERROR_GENERIC, # type: ignore
|
|
117
|
-
) -> None:
|
|
118
|
-
"""Send acn status error generic."""
|
|
119
|
-
acn_msg = acn_pb2.AcnMessage() # type: ignore
|
|
120
|
-
performative = acn_pb2.AcnMessage.Status_Performative() # type: ignore
|
|
121
|
-
status = AcnMessage.StatusBody(status_code=status_code, msgs=[msg])
|
|
122
|
-
AcnMessage.StatusBody.encode(
|
|
123
|
-
performative.body, status # pylint: disable=no-member
|
|
124
|
-
)
|
|
125
|
-
acn_msg.status.CopyFrom(performative) # pylint: disable=no-member
|
|
126
|
-
|
|
127
|
-
buf = acn_msg.SerializeToString()
|
|
128
|
-
|
|
129
|
-
await self._write(buf)
|
|
130
|
-
|
|
131
|
-
async def connect(self) -> bool:
|
|
132
|
-
"""Connect to node with pipe."""
|
|
133
|
-
return await self.pipe.connect()
|
|
134
|
-
|
|
135
|
-
async def send_envelope(self, envelope: Envelope) -> None:
|
|
136
|
-
"""Send envelope to node."""
|
|
137
|
-
self._wait_status = asyncio.Future()
|
|
138
|
-
buf = self.make_acn_envelope_message(envelope)
|
|
139
|
-
await self._write(buf)
|
|
140
|
-
try:
|
|
141
|
-
status = await self.wait_for_status()
|
|
142
|
-
if status.code != int(AcnMessage.StatusBody.StatusCode.SUCCESS): # type: ignore # pylint: disable=no-member
|
|
143
|
-
raise ValueError( # pragma: nocover
|
|
144
|
-
f"failed to send envelope. got error confirmation: {status}"
|
|
145
|
-
)
|
|
146
|
-
except asyncio.TimeoutError: # pragma: nocover
|
|
147
|
-
if not self._wait_status.done(): # pragma: nocover
|
|
148
|
-
self._wait_status.set_exception(Exception("Timeout"))
|
|
149
|
-
await asyncio.sleep(0)
|
|
150
|
-
raise ValueError("acn status await timeout!")
|
|
151
|
-
finally:
|
|
152
|
-
self._wait_status = None
|
|
153
|
-
|
|
154
|
-
def make_agent_record(self) -> AcnMessage.AgentRecord: # type: ignore
|
|
155
|
-
"""Make acn agent record."""
|
|
156
|
-
agent_record = AcnMessage.AgentRecord(
|
|
157
|
-
address=self.agent_record.address,
|
|
158
|
-
public_key=self.agent_record.public_key,
|
|
159
|
-
peer_public_key=self.agent_record.representative_public_key,
|
|
160
|
-
signature=self.agent_record.signature,
|
|
161
|
-
service_id=POR_DEFAULT_SERVICE_ID,
|
|
162
|
-
ledger_id=self.agent_record.ledger_id,
|
|
163
|
-
)
|
|
164
|
-
return agent_record
|
|
165
|
-
|
|
166
|
-
async def read_envelope(self) -> Optional[Envelope]:
|
|
167
|
-
"""Read envelope from the node."""
|
|
168
|
-
while True:
|
|
169
|
-
buf = await self._read()
|
|
170
|
-
|
|
171
|
-
if not buf:
|
|
172
|
-
return None
|
|
173
|
-
|
|
174
|
-
try:
|
|
175
|
-
acn_msg = acn_pb2.AcnMessage() # type: ignore
|
|
176
|
-
acn_msg.ParseFromString(buf)
|
|
177
|
-
|
|
178
|
-
except Exception as e: # pragma: nocover
|
|
179
|
-
await self.write_acn_status_error(
|
|
180
|
-
f"Failed to parse acn message {e}",
|
|
181
|
-
status_code=AcnMessage.StatusBody.StatusCode.ERROR_DECODE,
|
|
182
|
-
)
|
|
183
|
-
raise ValueError(f"Error parsing acn message: {e}") from e
|
|
184
|
-
|
|
185
|
-
performative = acn_msg.WhichOneof("performative")
|
|
186
|
-
if performative == "aea_envelope": # pragma: nocover
|
|
187
|
-
aea_envelope = acn_msg.aea_envelope # pylint: disable=no-member
|
|
188
|
-
try:
|
|
189
|
-
envelope = Envelope.decode(aea_envelope.envelope)
|
|
190
|
-
await self.write_acn_status_ok()
|
|
191
|
-
return envelope
|
|
192
|
-
except Exception as e:
|
|
193
|
-
await self.write_acn_status_error(
|
|
194
|
-
f"Failed to decode envelope: {e}",
|
|
195
|
-
status_code=AcnMessage.StatusBody.StatusCode.ERROR_DECODE,
|
|
196
|
-
)
|
|
197
|
-
raise
|
|
198
|
-
|
|
199
|
-
elif performative == "status":
|
|
200
|
-
if self._wait_status is not None:
|
|
201
|
-
self._wait_status.set_result(
|
|
202
|
-
acn_msg.status.body # pylint: disable=no-member
|
|
203
|
-
)
|
|
204
|
-
else: # pragma: nocover
|
|
205
|
-
await self.write_acn_status_error(
|
|
206
|
-
f"Bad acn message {performative}",
|
|
207
|
-
status_code=AcnMessage.StatusBody.StatusCode.ERROR_UNEXPECTED_PAYLOAD,
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
async def _write(self, data: bytes) -> None:
|
|
211
|
-
"""
|
|
212
|
-
Write to the writer stream.
|
|
213
|
-
|
|
214
|
-
:param data: data to write to stream
|
|
215
|
-
"""
|
|
216
|
-
await self.pipe.write(data)
|
|
217
|
-
|
|
218
|
-
async def _read(self) -> Optional[bytes]:
|
|
219
|
-
"""
|
|
220
|
-
Read from the reader stream.
|
|
221
|
-
|
|
222
|
-
:return: bytes
|
|
223
|
-
"""
|
|
224
|
-
return await self.pipe.read()
|
|
225
|
-
|
|
226
|
-
async def register(
|
|
227
|
-
self,
|
|
228
|
-
) -> None:
|
|
229
|
-
"""Register agent on the remote node."""
|
|
230
|
-
agent_record = self.make_agent_record()
|
|
231
|
-
acn_msg = acn_pb2.AcnMessage() # type: ignore
|
|
232
|
-
performative = acn_pb2.AcnMessage.Register_Performative() # type: ignore
|
|
233
|
-
AcnMessage.AgentRecord.encode(
|
|
234
|
-
performative.record, agent_record # pylint: disable=no-member
|
|
235
|
-
)
|
|
236
|
-
acn_msg.register.CopyFrom(performative) # pylint: disable=no-member
|
|
237
|
-
|
|
238
|
-
buf = acn_msg.SerializeToString()
|
|
239
|
-
await self._write(buf)
|
|
240
|
-
|
|
241
|
-
try:
|
|
242
|
-
buf = await asyncio.wait_for(self._read(), timeout=self.ACN_ACK_TIMEOUT)
|
|
243
|
-
except ConnectionError as e: # pragma: nocover
|
|
244
|
-
raise e
|
|
245
|
-
except IncompleteReadError as e: # pragma: no cover
|
|
246
|
-
raise e
|
|
247
|
-
|
|
248
|
-
if buf is None: # pragma: nocover
|
|
249
|
-
raise ConnectionError(
|
|
250
|
-
"Error on connection setup. Incoming buffer is empty!"
|
|
251
|
-
)
|
|
252
|
-
acn_msg = acn_pb2.AcnMessage() # type: ignore
|
|
253
|
-
acn_msg.ParseFromString(buf)
|
|
254
|
-
performative = acn_msg.WhichOneof("performative")
|
|
255
|
-
if performative != "status": # pragma: nocover
|
|
256
|
-
raise Exception(f"Wrong response message from peer: {performative}")
|
|
257
|
-
response = acn_msg.status # pylint: disable=no-member
|
|
258
|
-
|
|
259
|
-
if response.body.code != int(AcnMessage.StatusBody.StatusCode.SUCCESS): # type: ignore # pylint: disable=no-member
|
|
260
|
-
raise Exception( # pragma: nocover
|
|
261
|
-
"Registration to peer failed: {}".format(
|
|
262
|
-
AcnMessage.StatusBody.StatusCode(response.body.code) # type: ignore # pylint: disable=no-member
|
|
263
|
-
)
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
async def close(self) -> None:
|
|
267
|
-
"""Close client and pipe."""
|
|
268
|
-
await self.pipe.close()
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
class P2PLibp2pClientConnection(Connection):
|
|
272
|
-
"""
|
|
273
|
-
A libp2p client connection.
|
|
274
|
-
|
|
275
|
-
Send and receive envelopes to and from agents on the p2p network without deploying a libp2p node.
|
|
276
|
-
Connect to the libp2p node using traffic delegation service.
|
|
277
|
-
"""
|
|
278
|
-
|
|
279
|
-
connection_id = PUBLIC_ID
|
|
280
|
-
|
|
281
|
-
DEFAULT_CONNECT_RETRIES = 3
|
|
282
|
-
DEFAULT_RESEND_ENVELOPE_RETRY = 1
|
|
283
|
-
DEFAULT_TLS_CONNECTION_SIGNATURE_TIMEOUT = 5.0
|
|
284
|
-
|
|
285
|
-
def __init__(self, **kwargs: Any) -> None:
|
|
286
|
-
"""Initialize a libp2p client connection."""
|
|
287
|
-
super().__init__(**kwargs)
|
|
288
|
-
|
|
289
|
-
self.tls_connection_signature_timeout = self.configuration.config.get(
|
|
290
|
-
"tls_connection_signature_timeout",
|
|
291
|
-
self.DEFAULT_TLS_CONNECTION_SIGNATURE_TIMEOUT,
|
|
292
|
-
)
|
|
293
|
-
self.connect_retries = self.configuration.config.get(
|
|
294
|
-
"connect_retries", self.DEFAULT_CONNECT_RETRIES
|
|
295
|
-
)
|
|
296
|
-
self.resend_envelope_retry = self.configuration.config.get(
|
|
297
|
-
"resend_envelope_retry", self.DEFAULT_RESEND_ENVELOPE_RETRY
|
|
298
|
-
)
|
|
299
|
-
ledger_id = self.configuration.config.get("ledger_id", DEFAULT_LEDGER)
|
|
300
|
-
if ledger_id not in SUPPORTED_LEDGER_IDS:
|
|
301
|
-
raise ValueError( # pragma: nocover
|
|
302
|
-
"Ledger id '{}' is not supported. Supported ids: '{}'".format(
|
|
303
|
-
ledger_id, SUPPORTED_LEDGER_IDS
|
|
304
|
-
)
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
key_file: Optional[str] = self.configuration.config.get("tcp_key_file")
|
|
308
|
-
nodes: Optional[List[Dict[str, Any]]] = self.configuration.config.get("nodes")
|
|
309
|
-
|
|
310
|
-
if nodes is None:
|
|
311
|
-
raise ValueError("At least one node should be provided")
|
|
312
|
-
nodes = list(nodes)
|
|
313
|
-
|
|
314
|
-
nodes_uris = [node.get("uri", None) for node in nodes]
|
|
315
|
-
enforce(
|
|
316
|
-
len(nodes_uris) == len(nodes) and None not in nodes_uris,
|
|
317
|
-
"Delegate 'uri' should be provided for each node",
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
nodes_public_keys = [node.get("public_key", None) for node in nodes]
|
|
321
|
-
enforce(
|
|
322
|
-
len(nodes_public_keys) == len(nodes) and None not in nodes_public_keys,
|
|
323
|
-
"Delegate 'public_key' should be provided for each node",
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
cert_requests = self.configuration.cert_requests
|
|
327
|
-
if cert_requests is None or len(cert_requests) != len(nodes):
|
|
328
|
-
raise ValueError( # pragma: nocover
|
|
329
|
-
"cert_requests field must be set and contain exactly as many entries as 'nodes'!"
|
|
330
|
-
)
|
|
331
|
-
for cert_request in cert_requests:
|
|
332
|
-
save_path = cert_request.get_absolute_save_path(Path(self.data_dir))
|
|
333
|
-
if not save_path.is_file():
|
|
334
|
-
raise Exception( # pragma: nocover
|
|
335
|
-
f"cert_request 'save_path' field is not a file:\n{save_path}\n"
|
|
336
|
-
"Please ensure that 'issue-certificates' command is called beforehand"
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
# we cannot use the key from the connection's crypto store as
|
|
340
|
-
# the key will be used for TLS tcp connection, whereas the
|
|
341
|
-
# connection's crypto store key is used for PoR
|
|
342
|
-
if key_file is not None:
|
|
343
|
-
key = make_crypto(ledger_id, private_key_path=key_file)
|
|
344
|
-
else:
|
|
345
|
-
key = make_crypto(ledger_id)
|
|
346
|
-
|
|
347
|
-
# client connection id
|
|
348
|
-
self.key = key
|
|
349
|
-
self.logger.debug("Public key used for TCP: {}".format(key.public_key))
|
|
350
|
-
|
|
351
|
-
# delegate uris
|
|
352
|
-
self.delegate_uris = [Uri(node_uri) for node_uri in nodes_uris]
|
|
353
|
-
|
|
354
|
-
# delegates PoRs
|
|
355
|
-
self.delegate_pors: List[AgentRecord] = []
|
|
356
|
-
for i, cert_request in enumerate(cert_requests):
|
|
357
|
-
agent_record = AgentRecord.from_cert_request(
|
|
358
|
-
cert_request, self.address, nodes_public_keys[i], self.data_dir
|
|
359
|
-
)
|
|
360
|
-
self.delegate_pors.append(agent_record)
|
|
361
|
-
|
|
362
|
-
# select a delegate
|
|
363
|
-
index = random.randint(0, len(self.delegate_uris) - 1) # nosec
|
|
364
|
-
self.node_uri = self.delegate_uris[index]
|
|
365
|
-
self.node_por = self.delegate_pors[index]
|
|
366
|
-
self.logger.debug("Node to use as delegate: {}".format(self.node_uri))
|
|
367
|
-
|
|
368
|
-
self._in_queue = None # type: Optional[asyncio.Queue]
|
|
369
|
-
self._process_messages_task = None # type: Optional[asyncio.Future]
|
|
370
|
-
self._node_client: Optional[NodeClient] = None
|
|
371
|
-
|
|
372
|
-
self._send_queue: Optional[asyncio.Queue] = None
|
|
373
|
-
self._send_task: Optional[asyncio.Task] = None
|
|
374
|
-
|
|
375
|
-
async def _send_loop(self) -> None:
|
|
376
|
-
"""Handle message in the send queue."""
|
|
377
|
-
|
|
378
|
-
if not self._send_queue or not self._node_client: # pragma: nocover
|
|
379
|
-
self.logger.error("Send loop not started cause not connected properly.")
|
|
380
|
-
return
|
|
381
|
-
try:
|
|
382
|
-
while self.is_connected:
|
|
383
|
-
envelope = await self._send_queue.get()
|
|
384
|
-
await self._send_envelope_with_node_client(envelope)
|
|
385
|
-
except asyncio.CancelledError: # pylint: disable=try-except-raise
|
|
386
|
-
raise # pragma: nocover
|
|
387
|
-
except Exception: # pylint: disable=broad-except # pragma: nocover
|
|
388
|
-
self.logger.exception(
|
|
389
|
-
f"Failed to send an envelope {envelope}. Stop connection."
|
|
390
|
-
)
|
|
391
|
-
await asyncio.shield(self.disconnect())
|
|
392
|
-
|
|
393
|
-
async def _send_envelope_with_node_client(
|
|
394
|
-
self, envelope: Envelope, retry_counter: int = 0
|
|
395
|
-
) -> None:
|
|
396
|
-
"""Send envelope with node client, reconnect and retry on fail."""
|
|
397
|
-
if not self._node_client: # pragma: nocover
|
|
398
|
-
raise ValueError("Connection not connected to node!")
|
|
399
|
-
if retry_counter > self.resend_envelope_retry:
|
|
400
|
-
self.logger.warning(
|
|
401
|
-
f"Dropping envelope {envelope}. It failed after retry. "
|
|
402
|
-
)
|
|
403
|
-
return
|
|
404
|
-
self._ensure_valid_envelope_for_external_comms(envelope)
|
|
405
|
-
try:
|
|
406
|
-
await self._node_client.send_envelope(envelope)
|
|
407
|
-
except Exception: # pylint: disable=broad-except
|
|
408
|
-
self.logger.exception(
|
|
409
|
-
"Exception raised on message send. Try reconnect and send again."
|
|
410
|
-
)
|
|
411
|
-
await self._perform_connection_to_node()
|
|
412
|
-
await self._send_envelope_with_node_client(envelope, retry_counter + 1)
|
|
413
|
-
|
|
414
|
-
async def connect(self) -> None:
|
|
415
|
-
"""Set up the connection."""
|
|
416
|
-
if self.is_connected: # pragma: nocover
|
|
417
|
-
return
|
|
418
|
-
|
|
419
|
-
with self._connect_context():
|
|
420
|
-
# connect libp2p client
|
|
421
|
-
|
|
422
|
-
await self._perform_connection_to_node()
|
|
423
|
-
# start receiving msgs
|
|
424
|
-
self._in_queue = asyncio.Queue()
|
|
425
|
-
self._process_messages_task = asyncio.ensure_future(
|
|
426
|
-
self._process_messages(), loop=self.loop
|
|
427
|
-
)
|
|
428
|
-
self._send_queue = asyncio.Queue()
|
|
429
|
-
self._send_task = self.loop.create_task(self._send_loop())
|
|
430
|
-
|
|
431
|
-
async def _perform_connection_to_node(self) -> None:
|
|
432
|
-
"""Connect to node with retries."""
|
|
433
|
-
for attempt in range(self.connect_retries):
|
|
434
|
-
if self.state not in [ # type: ignore
|
|
435
|
-
ConnectionStates.connecting,
|
|
436
|
-
ConnectionStates.connected,
|
|
437
|
-
]:
|
|
438
|
-
# do nothing if disconnected, or disconnecting
|
|
439
|
-
return # pragma: nocover
|
|
440
|
-
try:
|
|
441
|
-
self.logger.info(
|
|
442
|
-
"Connecting to libp2p node {}. Attempt {}".format(
|
|
443
|
-
str(self.node_uri), attempt + 1
|
|
444
|
-
)
|
|
445
|
-
)
|
|
446
|
-
pipe = TCPSocketChannelClientTLS(
|
|
447
|
-
f"{self.node_uri.host}:{self.node_uri._port}", # pylint: disable=protected-access
|
|
448
|
-
"",
|
|
449
|
-
server_pub_key=self.node_por.representative_public_key,
|
|
450
|
-
verification_signature_wait_timeout=self.tls_connection_signature_timeout,
|
|
451
|
-
)
|
|
452
|
-
if not await pipe.connect():
|
|
453
|
-
raise ValueError(
|
|
454
|
-
f"Pipe connection error: {pipe.last_exception or ''}"
|
|
455
|
-
)
|
|
456
|
-
|
|
457
|
-
self._node_client = NodeClient(pipe, self.node_por)
|
|
458
|
-
await self._setup_connection()
|
|
459
|
-
|
|
460
|
-
self.logger.info(
|
|
461
|
-
"Successfully connected to libp2p node {}".format(
|
|
462
|
-
str(self.node_uri)
|
|
463
|
-
)
|
|
464
|
-
)
|
|
465
|
-
return
|
|
466
|
-
except Exception as e: # pylint: disable=broad-except
|
|
467
|
-
if attempt == self.connect_retries - 1:
|
|
468
|
-
self.logger.error(
|
|
469
|
-
"Connection to libp2p node {} failed: error: {}. It was the last attempt, exception will be raised".format(
|
|
470
|
-
str(self.node_uri), str(e)
|
|
471
|
-
)
|
|
472
|
-
)
|
|
473
|
-
self.state = ConnectionStates.disconnected
|
|
474
|
-
raise
|
|
475
|
-
sleep_time = attempt * 2 + 1
|
|
476
|
-
self.logger.error(
|
|
477
|
-
"Connection to libp2p node {} failed: error: {}. Another attempt will be performed in {} seconds".format(
|
|
478
|
-
str(self.node_uri), str(e), sleep_time
|
|
479
|
-
)
|
|
480
|
-
)
|
|
481
|
-
await asyncio.sleep(sleep_time)
|
|
482
|
-
|
|
483
|
-
async def _setup_connection(self) -> None:
|
|
484
|
-
"""Set up connection to node over tcp connection."""
|
|
485
|
-
if not self._node_client: # pragma: nocover
|
|
486
|
-
raise ValueError("Connection was not connected!")
|
|
487
|
-
|
|
488
|
-
await self._node_client.register()
|
|
489
|
-
|
|
490
|
-
async def disconnect(self) -> None:
|
|
491
|
-
"""Disconnect from the channel."""
|
|
492
|
-
if self.is_disconnected: # pragma: nocover
|
|
493
|
-
return
|
|
494
|
-
|
|
495
|
-
self.state = ConnectionStates.disconnecting
|
|
496
|
-
self.logger.debug("disconnecting libp2p client connection...")
|
|
497
|
-
|
|
498
|
-
if self._process_messages_task is not None:
|
|
499
|
-
if not self._process_messages_task.done():
|
|
500
|
-
self._process_messages_task.cancel()
|
|
501
|
-
self._process_messages_task = None
|
|
502
|
-
|
|
503
|
-
if self._send_task is not None:
|
|
504
|
-
if not self._send_task.done():
|
|
505
|
-
self._send_task.cancel()
|
|
506
|
-
self._send_task = None
|
|
507
|
-
|
|
508
|
-
try:
|
|
509
|
-
self.logger.debug("disconnecting libp2p node client connection...")
|
|
510
|
-
if self._node_client is not None:
|
|
511
|
-
await self._node_client.close()
|
|
512
|
-
except Exception: # pragma: nocover # pylint:disable=broad-except
|
|
513
|
-
self.logger.exception("exception on node client close")
|
|
514
|
-
raise
|
|
515
|
-
finally:
|
|
516
|
-
# set disconnected state anyway
|
|
517
|
-
if self._in_queue is not None:
|
|
518
|
-
self._in_queue.put_nowait(None)
|
|
519
|
-
|
|
520
|
-
self.state = ConnectionStates.disconnected
|
|
521
|
-
self.logger.debug("libp2p client connection disconnected.")
|
|
522
|
-
|
|
523
|
-
async def receive(self, *args: Any, **kwargs: Any) -> Optional["Envelope"]:
|
|
524
|
-
"""
|
|
525
|
-
Receive an envelope. Blocking.
|
|
526
|
-
|
|
527
|
-
:param args: positional arguments
|
|
528
|
-
:param kwargs: keyword arguments
|
|
529
|
-
:return: the envelope received, or None.
|
|
530
|
-
"""
|
|
531
|
-
try:
|
|
532
|
-
if self._in_queue is None:
|
|
533
|
-
raise ValueError("Input queue not initialized.") # pragma: nocover
|
|
534
|
-
envelope = await self._in_queue.get()
|
|
535
|
-
if envelope is None: # pragma: no cover
|
|
536
|
-
self.logger.debug("Received None.")
|
|
537
|
-
return None
|
|
538
|
-
self.logger.debug("Received envelope: {}".format(envelope))
|
|
539
|
-
return envelope
|
|
540
|
-
except CancelledError: # pragma: no cover
|
|
541
|
-
self.logger.debug("Receive cancelled.")
|
|
542
|
-
return None
|
|
543
|
-
except Exception as e: # pragma: no cover # pylint: disable=broad-except
|
|
544
|
-
self.logger.exception(e)
|
|
545
|
-
return None
|
|
546
|
-
|
|
547
|
-
async def send(self, envelope: Envelope) -> None:
|
|
548
|
-
"""
|
|
549
|
-
Send messages.
|
|
550
|
-
|
|
551
|
-
:param envelope: the envelope
|
|
552
|
-
"""
|
|
553
|
-
if not self._node_client or not self._send_queue:
|
|
554
|
-
raise ValueError("Node is not connected!") # pragma: nocover
|
|
555
|
-
|
|
556
|
-
self._ensure_valid_envelope_for_external_comms(envelope)
|
|
557
|
-
await self._send_queue.put(envelope)
|
|
558
|
-
|
|
559
|
-
async def _read_envelope_from_node(self) -> Optional[Envelope]:
|
|
560
|
-
"""Read envelope from node, reconnec on error."""
|
|
561
|
-
if not self._node_client: # pragma: nocover
|
|
562
|
-
raise ValueError("Connection not connected to node!")
|
|
563
|
-
|
|
564
|
-
try:
|
|
565
|
-
self.logger.debug("Waiting for messages...")
|
|
566
|
-
envelope = await self._node_client.read_envelope()
|
|
567
|
-
return envelope
|
|
568
|
-
except ConnectionError as e: # pragma: nocover
|
|
569
|
-
self.logger.error(f"Connection error: {e}. Try to reconnect and read again")
|
|
570
|
-
except IncompleteReadError as e: # pragma: no cover
|
|
571
|
-
self.logger.error(
|
|
572
|
-
"Connection disconnected while reading from node ({}/{})".format(
|
|
573
|
-
len(e.partial), e.expected
|
|
574
|
-
)
|
|
575
|
-
)
|
|
576
|
-
except Exception as e: # pylint: disable=broad-except # pragma: nocover
|
|
577
|
-
self.logger.exception(f"On envelope read: {e}")
|
|
578
|
-
|
|
579
|
-
try:
|
|
580
|
-
self.logger.debug("Read envelope retry! Reconnect first!")
|
|
581
|
-
await self._perform_connection_to_node()
|
|
582
|
-
envelope = await self._node_client.read_envelope()
|
|
583
|
-
return envelope
|
|
584
|
-
except Exception: # pragma: no cover # pylint: disable=broad-except
|
|
585
|
-
self.logger.exception("Failed to read with reconnect!")
|
|
586
|
-
return None
|
|
587
|
-
|
|
588
|
-
async def _process_messages(self) -> None:
|
|
589
|
-
"""Receive data from node."""
|
|
590
|
-
if not self._node_client: # pragma: nocover
|
|
591
|
-
raise ValueError("Connection not connected to node!")
|
|
592
|
-
|
|
593
|
-
while True:
|
|
594
|
-
envelope = await self._read_envelope_from_node()
|
|
595
|
-
if self._in_queue is None:
|
|
596
|
-
raise ValueError("Input queue not initialized.") # pragma: nocover
|
|
597
|
-
self.logger.debug(f"Received envelope: {envelope}")
|
|
598
|
-
if envelope is None:
|
|
599
|
-
# give it time to recover
|
|
600
|
-
# twice the amount what we wait for ACK timeouts
|
|
601
|
-
timeout = NodeClient.ACN_ACK_TIMEOUT * 2
|
|
602
|
-
await asyncio.sleep(timeout)
|
|
603
|
-
continue # pragma: no cover
|
|
604
|
-
self._in_queue.put_nowait(envelope)
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
class TCPSocketChannelClientTLS(TCPSocketChannelClient):
|
|
608
|
-
"""Interprocess communication channel client using tcp sockets with TLS."""
|
|
609
|
-
|
|
610
|
-
DEFAULT_VERIFICATION_SIGNATURE_WAIT_TIMEOUT = 5.0
|
|
611
|
-
|
|
612
|
-
def __init__(
|
|
613
|
-
self,
|
|
614
|
-
in_path: str,
|
|
615
|
-
out_path: str,
|
|
616
|
-
server_pub_key: str,
|
|
617
|
-
logger: logging.Logger = _default_logger,
|
|
618
|
-
loop: Optional[AbstractEventLoop] = None,
|
|
619
|
-
verification_signature_wait_timeout: Optional[float] = None,
|
|
620
|
-
) -> None:
|
|
621
|
-
"""
|
|
622
|
-
Initialize a tcp socket communication channel client.
|
|
623
|
-
|
|
624
|
-
:param in_path: rendezvous point for incoming data
|
|
625
|
-
:param out_path: rendezvous point for outgoing data
|
|
626
|
-
:param server_pub_key: str, server public key to verify identity
|
|
627
|
-
:param logger: the logger
|
|
628
|
-
:param loop: the event loop
|
|
629
|
-
:param verification_signature_wait_timeout: optional float, if not provided, default value will be used
|
|
630
|
-
"""
|
|
631
|
-
super().__init__(in_path, out_path, logger, loop)
|
|
632
|
-
self.verification_signature_wait_timeout = (
|
|
633
|
-
self.DEFAULT_VERIFICATION_SIGNATURE_WAIT_TIMEOUT
|
|
634
|
-
if verification_signature_wait_timeout is None
|
|
635
|
-
else verification_signature_wait_timeout
|
|
636
|
-
)
|
|
637
|
-
self.server_pub_key = server_pub_key
|
|
638
|
-
|
|
639
|
-
@staticmethod
|
|
640
|
-
def _get_session_pub_key(writer: StreamWriter) -> bytes: # pragma: nocover
|
|
641
|
-
"""Get session public key from tls stream writer."""
|
|
642
|
-
cert_data = writer.get_extra_info("ssl_object").getpeercert(binary_form=True)
|
|
643
|
-
|
|
644
|
-
cert = x509.Certificate.load(cert_data)
|
|
645
|
-
session_pub_key = VerifyingKey.from_der(cert.public_key.dump()).to_string(
|
|
646
|
-
"uncompressed"
|
|
647
|
-
)
|
|
648
|
-
return session_pub_key
|
|
649
|
-
|
|
650
|
-
async def _open_connection(self) -> TCPSocketProtocol:
|
|
651
|
-
"""Open a connection with TLS support and verify peer."""
|
|
652
|
-
sock = await self._open_tls_connection()
|
|
653
|
-
session_pub_key = self._get_session_pub_key(sock.writer)
|
|
654
|
-
|
|
655
|
-
try:
|
|
656
|
-
signature = await asyncio.wait_for(
|
|
657
|
-
sock.read(), timeout=self.verification_signature_wait_timeout
|
|
658
|
-
)
|
|
659
|
-
except asyncio.TimeoutError: # pragma: nocover
|
|
660
|
-
raise ValueError(
|
|
661
|
-
f"Failed to get peer verification record in timeout: {self.verification_signature_wait_timeout}"
|
|
662
|
-
)
|
|
663
|
-
|
|
664
|
-
if not signature: # pragma: nocover
|
|
665
|
-
raise ValueError("Unexpected socket read data!")
|
|
666
|
-
|
|
667
|
-
try:
|
|
668
|
-
self._verify_session_key_signature(signature, session_pub_key)
|
|
669
|
-
except BadSignatureError as e: # pragma: nocover
|
|
670
|
-
with contextlib.suppress(Exception):
|
|
671
|
-
await sock.close()
|
|
672
|
-
raise ValueError(f"Invalid TLS session key signature: {e}")
|
|
673
|
-
return sock
|
|
674
|
-
|
|
675
|
-
async def _open_tls_connection(self) -> TCPSocketProtocol:
|
|
676
|
-
"""Open a connection with TLS support."""
|
|
677
|
-
cadata = await asyncio.get_event_loop().run_in_executor(
|
|
678
|
-
None, lambda: ssl.get_server_certificate((self._host, self._port))
|
|
679
|
-
)
|
|
680
|
-
|
|
681
|
-
ssl_ctx = ssl.create_default_context(cadata=cadata)
|
|
682
|
-
ssl_ctx.check_hostname = False
|
|
683
|
-
ssl_ctx.verify_mode = ssl.CERT_REQUIRED
|
|
684
|
-
reader, writer = await asyncio.open_connection(
|
|
685
|
-
self._host,
|
|
686
|
-
self._port,
|
|
687
|
-
ssl=ssl_ctx,
|
|
688
|
-
)
|
|
689
|
-
return TCPSocketProtocol(reader, writer, logger=self.logger, loop=self._loop)
|
|
690
|
-
|
|
691
|
-
def _verify_session_key_signature(
|
|
692
|
-
self, signature: bytes, session_pub_key: bytes
|
|
693
|
-
) -> None:
|
|
694
|
-
"""
|
|
695
|
-
Validate signature of session public key.
|
|
696
|
-
|
|
697
|
-
:param signature: bytes, signature of session public key made with server private key
|
|
698
|
-
:param session_pub_key: session public key to check signature for.
|
|
699
|
-
"""
|
|
700
|
-
vk = VerifyingKey.from_string(bytes.fromhex(self.server_pub_key), SECP256k1)
|
|
701
|
-
vk.verify(
|
|
702
|
-
signature, session_pub_key, hashfunc=hashlib.sha256, sigdecode=sigdecode_der
|
|
703
|
-
)
|