distributed-state-network 0.0.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.
- distributed_state_network/__init__.py +4 -0
- distributed_state_network/create_key.py +10 -0
- distributed_state_network/dsnode.py +263 -0
- distributed_state_network/handler.py +114 -0
- distributed_state_network/objects/config.py +15 -0
- distributed_state_network/objects/endpoint.py +36 -0
- distributed_state_network/objects/hello_packet.py +59 -0
- distributed_state_network/objects/peers_packet.py +41 -0
- distributed_state_network/objects/signed_packet.py +20 -0
- distributed_state_network/objects/state_packet.py +64 -0
- distributed_state_network/util/__init__.py +34 -0
- distributed_state_network/util/aes.py +45 -0
- distributed_state_network/util/byte_helper.py +36 -0
- distributed_state_network/util/ecdsa.py +21 -0
- distributed_state_network/util/https.py +54 -0
- distributed_state_network/util/key_manager.py +86 -0
- distributed_state_network-0.0.1.dist-info/METADATA +149 -0
- distributed_state_network-0.0.1.dist-info/RECORD +20 -0
- distributed_state_network-0.0.1.dist-info/WHEEL +4 -0
- distributed_state_network-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
import threading
|
|
8
|
+
from requests import RequestException
|
|
9
|
+
from typing import Dict, Tuple, List, Optional
|
|
10
|
+
|
|
11
|
+
from distributed_state_network.objects.endpoint import Endpoint
|
|
12
|
+
from distributed_state_network.objects.hello_packet import HelloPacket
|
|
13
|
+
from distributed_state_network.objects.peers_packet import PeersPacket
|
|
14
|
+
from distributed_state_network.objects.state_packet import StatePacket
|
|
15
|
+
from distributed_state_network.objects.config import DSNodeConfig
|
|
16
|
+
|
|
17
|
+
from distributed_state_network.util import get_dict_hash
|
|
18
|
+
from distributed_state_network.util.key_manager import CertManager, CredentialManager
|
|
19
|
+
from distributed_state_network.util.aes import aes_encrypt, aes_decrypt, generate_aes_key
|
|
20
|
+
|
|
21
|
+
TICK_INTERVAL = 3
|
|
22
|
+
|
|
23
|
+
class DSNode:
|
|
24
|
+
config: DSNodeConfig
|
|
25
|
+
address_book: Dict[str, Endpoint]
|
|
26
|
+
node_states: Dict[str, StatePacket]
|
|
27
|
+
shutting_down: bool = False
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
config: DSNodeConfig,
|
|
32
|
+
version: str,
|
|
33
|
+
):
|
|
34
|
+
self.config = config
|
|
35
|
+
self.version = version
|
|
36
|
+
|
|
37
|
+
self.cert_manager = CertManager(config.node_id)
|
|
38
|
+
self.cred_manager = CredentialManager(config.node_id)
|
|
39
|
+
|
|
40
|
+
self.cert_manager.generate_keys()
|
|
41
|
+
self.cred_manager.generate_keys()
|
|
42
|
+
|
|
43
|
+
self.node_states = {
|
|
44
|
+
self.config.node_id: StatePacket.create(self.config.node_id, time.time(), self.cred_manager.my_private(), { })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
self.address_book = {
|
|
48
|
+
self.config.node_id: Endpoint("127.0.0.1", config.port)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
self.logger = logging.getLogger("DSN: " + config.node_id)
|
|
52
|
+
if not os.path.exists(config.aes_key_file):
|
|
53
|
+
raise Exception(f"Could not find aes key file in {config.aes_key_file}")
|
|
54
|
+
threading.Thread(target=self.network_tick).start()
|
|
55
|
+
|
|
56
|
+
def get_aes_key(self):
|
|
57
|
+
with open(self.config.aes_key_file, 'rb') as f:
|
|
58
|
+
return f.read()
|
|
59
|
+
|
|
60
|
+
def network_tick(self):
|
|
61
|
+
time.sleep(TICK_INTERVAL)
|
|
62
|
+
if self.shutting_down:
|
|
63
|
+
self.logger.info(f"Shutting down node")
|
|
64
|
+
return
|
|
65
|
+
self.test_connections()
|
|
66
|
+
threading.Thread(target=self.network_tick).start()
|
|
67
|
+
|
|
68
|
+
def test_connections(self):
|
|
69
|
+
def remove(node_id: str):
|
|
70
|
+
if node_id in self.node_states:
|
|
71
|
+
del self.node_states[node_id]
|
|
72
|
+
self.logger.info(f"PING failed for {node_id}, disconnecting...")
|
|
73
|
+
for node_id in self.node_states.copy().keys():
|
|
74
|
+
if not node_id in self.node_states or node_id == self.config.node_id:
|
|
75
|
+
continue
|
|
76
|
+
try:
|
|
77
|
+
if self.shutting_down:
|
|
78
|
+
return
|
|
79
|
+
self.send_ping(node_id)
|
|
80
|
+
except RequestException:
|
|
81
|
+
if node_id in self.node_states: # double check if something has changed since the ping request started
|
|
82
|
+
remove(node_id)
|
|
83
|
+
|
|
84
|
+
def send_request_to_node(self, node_id: str, path: str, payload: bytes, verify) -> Tuple[requests.Response, bytes]:
|
|
85
|
+
con = self.connection_from_node(node_id)
|
|
86
|
+
return self.send_request(con, path, payload, verify)
|
|
87
|
+
|
|
88
|
+
def send_request(self, con: Endpoint, path: str, payload: bytes, verify, retries: int = 0) -> Tuple[requests.Response, bytes]:
|
|
89
|
+
try:
|
|
90
|
+
# Always send a ping first to throw an error if https validation does not work
|
|
91
|
+
requests.post(f'https://{con.to_string()}/ping', data=self.encrypt_data(payload), verify=verify, timeout=2)
|
|
92
|
+
res = requests.post(f'https://{con.to_string()}/{path}', data=self.encrypt_data(payload), verify=verify, timeout=2)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
self.logger.error(e)
|
|
95
|
+
time.sleep(1)
|
|
96
|
+
if retries < 2:
|
|
97
|
+
return self.send_request(con, path, payload, verify, retries + 1)
|
|
98
|
+
else:
|
|
99
|
+
raise RequestException(f'{path.upper()} => {con.to_string()} (no response)')
|
|
100
|
+
return self.parse_response(con, path, res)
|
|
101
|
+
|
|
102
|
+
def parse_response(self, con: Endpoint, path: str, res: requests.Response) -> Tuple[requests.Response, bytes]:
|
|
103
|
+
if res.status_code != 200:
|
|
104
|
+
raise RequestException(f'{path.upper()} => {con.to_string()} (status code {res.status_code})')
|
|
105
|
+
|
|
106
|
+
decrypted_data = b''
|
|
107
|
+
if len(res.content) > 0:
|
|
108
|
+
try:
|
|
109
|
+
decrypted_data = self.decrypt_data(res.content)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
raise RequestException(f'{path.upper()} => {con.to_string()} (cannot decrypt response)')
|
|
112
|
+
|
|
113
|
+
return res, decrypted_data
|
|
114
|
+
|
|
115
|
+
def encrypt_data(self, data: bytes) -> bytes:
|
|
116
|
+
return aes_encrypt(self.get_aes_key(), data)
|
|
117
|
+
|
|
118
|
+
def decrypt_data(self, data: bytes) -> bytes:
|
|
119
|
+
return aes_decrypt(self.get_aes_key(), data)
|
|
120
|
+
|
|
121
|
+
def request_peers(self, node_id: str):
|
|
122
|
+
pkt = PeersPacket(self.config.node_id, None, { })
|
|
123
|
+
pkt.sign(self.cred_manager.my_private())
|
|
124
|
+
res, content = self.send_request_to_node(node_id, 'peers', pkt.to_bytes(), self.cert_manager.public_path(node_id))
|
|
125
|
+
pkt = PeersPacket.from_bytes(content)
|
|
126
|
+
if not pkt.verify_signature(self.cred_manager.read_public(node_id)):
|
|
127
|
+
raise Exception("Could not verify peers packet")
|
|
128
|
+
|
|
129
|
+
for key in pkt.connections.keys():
|
|
130
|
+
if key == self.config.node_id:
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
self.address_book[key] = pkt.connections[key]
|
|
134
|
+
|
|
135
|
+
if key not in self.node_states:
|
|
136
|
+
self.send_hello(self.address_book[key])
|
|
137
|
+
|
|
138
|
+
_, node_state = self.send_update(key)
|
|
139
|
+
self.handle_update(node_state)
|
|
140
|
+
|
|
141
|
+
def handle_peers(self, data: bytes):
|
|
142
|
+
pkt = PeersPacket.from_bytes(data)
|
|
143
|
+
if pkt.node_id not in self.address_book:
|
|
144
|
+
raise Exception(401) # Not Authorized
|
|
145
|
+
|
|
146
|
+
if not pkt.verify_signature(self.cred_manager.read_public(pkt.node_id)):
|
|
147
|
+
raise Exception(406) # Not Acceptable
|
|
148
|
+
|
|
149
|
+
peers = { }
|
|
150
|
+
for key in self.address_book.keys():
|
|
151
|
+
peers[key] = self.address_book[key]
|
|
152
|
+
|
|
153
|
+
pkt = PeersPacket(self.config.node_id, None, peers)
|
|
154
|
+
pkt.sign(self.cred_manager.my_private())
|
|
155
|
+
return pkt.to_bytes()
|
|
156
|
+
|
|
157
|
+
def send_hello(self, con: Endpoint):
|
|
158
|
+
self.logger.info(f"HELLO => {con.to_string()}")
|
|
159
|
+
|
|
160
|
+
payload = self.my_hello_packet().to_bytes()
|
|
161
|
+
_, content = self.send_request(con, 'hello', payload, False)
|
|
162
|
+
self.handle_hello(content)
|
|
163
|
+
|
|
164
|
+
pkt = HelloPacket.from_bytes(content)
|
|
165
|
+
return pkt.node_id
|
|
166
|
+
|
|
167
|
+
def handle_hello(self, data: bytes) -> bytes:
|
|
168
|
+
pkt = HelloPacket.from_bytes(data)
|
|
169
|
+
self.logger.info(f"Received HELLO from {pkt.node_id}")
|
|
170
|
+
if pkt.version != self.version:
|
|
171
|
+
msg = f"HELLO => {pkt.node_id} (Version mismatch \"{pkt.version}\" != \"{self.version}\")"
|
|
172
|
+
self.logger.error(msg)
|
|
173
|
+
raise Exception(505) # Version not supported
|
|
174
|
+
|
|
175
|
+
self.cert_manager.ensure_public(pkt.node_id, pkt.https_certificate)
|
|
176
|
+
self.cred_manager.ensure_public(pkt.node_id, pkt.ecdsa_public_key)
|
|
177
|
+
|
|
178
|
+
if pkt.node_id not in self.address_book:
|
|
179
|
+
self.address_book[pkt.node_id] = pkt.connection
|
|
180
|
+
|
|
181
|
+
if pkt.node_id not in self.node_states:
|
|
182
|
+
self.node_states[pkt.node_id] = StatePacket(pkt.node_id, 0, b'', { })
|
|
183
|
+
|
|
184
|
+
return self.my_hello_packet().to_bytes()
|
|
185
|
+
|
|
186
|
+
def my_hello_packet(self) -> HelloPacket:
|
|
187
|
+
pkt = HelloPacket(
|
|
188
|
+
self.version,
|
|
189
|
+
self.config.node_id,
|
|
190
|
+
self.my_con(),
|
|
191
|
+
self.cred_manager.my_public(),
|
|
192
|
+
None,
|
|
193
|
+
self.cert_manager.my_public()
|
|
194
|
+
)
|
|
195
|
+
pkt.sign(self.cred_manager.my_private())
|
|
196
|
+
return pkt
|
|
197
|
+
|
|
198
|
+
def send_ping(self, node_id: str):
|
|
199
|
+
try:
|
|
200
|
+
self.send_request_to_node(node_id, 'ping', b' ', verify=self.cert_manager.public_path(node_id))
|
|
201
|
+
except Exception as e:
|
|
202
|
+
raise RequestException(f'PING => {node_id}: {e}')
|
|
203
|
+
|
|
204
|
+
def send_update(self, node_id: str):
|
|
205
|
+
self.logger.info(f"UPDATE => {node_id}")
|
|
206
|
+
return self.send_request_to_node(node_id, 'update', self.my_state().to_bytes(), self.cert_manager.public_path(node_id))
|
|
207
|
+
|
|
208
|
+
def handle_update(self, data: bytes):
|
|
209
|
+
pkt = StatePacket.from_bytes(data)
|
|
210
|
+
self.logger.info(f"Received UPDATE from {pkt.node_id}")
|
|
211
|
+
|
|
212
|
+
# ignore if we accidentally sent an update to ourselves
|
|
213
|
+
if pkt.node_id == self.config.node_id:
|
|
214
|
+
raise Exception(406) # Not acceptable
|
|
215
|
+
|
|
216
|
+
# don't use packets older than last update
|
|
217
|
+
if pkt.node_id in self.node_states and self.node_states[pkt.node_id].last_update > pkt.last_update:
|
|
218
|
+
raise Exception(406) # Not acceptable
|
|
219
|
+
|
|
220
|
+
if not pkt.verify_signature(self.cred_manager.read_public(pkt.node_id)):
|
|
221
|
+
raise Exception(401) # Not authorized
|
|
222
|
+
|
|
223
|
+
if pkt.node_id not in self.node_states:
|
|
224
|
+
self.node_states[pkt.node_id] = pkt
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
if get_dict_hash(self.node_states[pkt.node_id].state_data) != get_dict_hash(pkt.state_data):
|
|
228
|
+
self.node_states[pkt.node_id] = pkt
|
|
229
|
+
|
|
230
|
+
return self.my_state().to_bytes()
|
|
231
|
+
|
|
232
|
+
def my_state(self):
|
|
233
|
+
return self.node_states[self.config.node_id]
|
|
234
|
+
|
|
235
|
+
def bootstrap(self, con: Endpoint):
|
|
236
|
+
bootstrap_id = self.send_hello(con)
|
|
237
|
+
self.address_book[bootstrap_id] = con
|
|
238
|
+
_, content = self.send_update(bootstrap_id)
|
|
239
|
+
self.handle_update(content)
|
|
240
|
+
self.request_peers(bootstrap_id)
|
|
241
|
+
|
|
242
|
+
def connection_from_node(self, node_id: str) -> Endpoint:
|
|
243
|
+
if node_id not in self.address_book:
|
|
244
|
+
raise Exception(f"could not find connection for {node_id}")
|
|
245
|
+
return self.address_book[node_id]
|
|
246
|
+
|
|
247
|
+
def update_data(self, key: str, val: str):
|
|
248
|
+
self.node_states[self.config.node_id].update_state(key, val, self.cred_manager.my_private())
|
|
249
|
+
for key in list(self.node_states.keys())[:]:
|
|
250
|
+
if key == self.config.node_id:
|
|
251
|
+
continue
|
|
252
|
+
self.send_update(key)
|
|
253
|
+
|
|
254
|
+
def my_con(self) -> Endpoint:
|
|
255
|
+
return self.connection_from_node(self.config.node_id)
|
|
256
|
+
|
|
257
|
+
def read_data(self, node_id: str, key: str) -> Optional[str]:
|
|
258
|
+
if key not in self.node_states[node_id].state_data.keys():
|
|
259
|
+
return None
|
|
260
|
+
return self.node_states[node_id].state_data[key]
|
|
261
|
+
|
|
262
|
+
def peers(self) -> List[str]:
|
|
263
|
+
return list(self.node_states.keys())
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import ssl
|
|
2
|
+
import threading
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Tuple, Callable, Dict
|
|
6
|
+
from threading import Thread
|
|
7
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
8
|
+
|
|
9
|
+
from distributed_state_network.dsnode import DSNode
|
|
10
|
+
from distributed_state_network.objects.config import DSNodeConfig
|
|
11
|
+
from distributed_state_network.util.aes import generate_aes_key
|
|
12
|
+
from distributed_state_network.util import stop_thread
|
|
13
|
+
|
|
14
|
+
VERSION = "0.0.1"
|
|
15
|
+
logging.basicConfig(level=logging.INFO)
|
|
16
|
+
|
|
17
|
+
def respond_bytes(handler: BaseHTTPRequestHandler, data: bytes):
|
|
18
|
+
handler.send_response(200)
|
|
19
|
+
handler.send_header("Content-Type", "application/octet-stream")
|
|
20
|
+
handler.end_headers()
|
|
21
|
+
handler.wfile.write(data)
|
|
22
|
+
handler.wfile.flush()
|
|
23
|
+
|
|
24
|
+
def graceful_fail(httpd: BaseHTTPRequestHandler, body: bytes, fn: Callable):
|
|
25
|
+
try:
|
|
26
|
+
fn_result = fn(body)
|
|
27
|
+
if fn_result is not None:
|
|
28
|
+
respond_bytes(httpd, httpd.server.node.encrypt_data(fn_result))
|
|
29
|
+
else:
|
|
30
|
+
respond_bytes(httpd, b'')
|
|
31
|
+
except Exception as e:
|
|
32
|
+
httpd.server.node.logger.error(f"Sending Error: {e.args[0]}")
|
|
33
|
+
httpd.send_response(e.args[0])
|
|
34
|
+
httpd.end_headers()
|
|
35
|
+
|
|
36
|
+
class DSNodeHandler(BaseHTTPRequestHandler):
|
|
37
|
+
server: "NodeServer"
|
|
38
|
+
|
|
39
|
+
def do_POST(self):
|
|
40
|
+
content_length = int(self.headers.get('Content-Length', 0))
|
|
41
|
+
try:
|
|
42
|
+
body = self.server.node.decrypt_data(self.rfile.read(content_length))
|
|
43
|
+
except Exception as e:
|
|
44
|
+
self.server.node.logger.error(f"{self.path}: Error decrypting data, {e}")
|
|
45
|
+
respond_bytes(self, b'Not Authorized')
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if self.path == "/hello":
|
|
50
|
+
graceful_fail(self, body, self.server.node.handle_hello)
|
|
51
|
+
|
|
52
|
+
elif self.path == "/peers":
|
|
53
|
+
graceful_fail(self, body, self.server.node.handle_peers)
|
|
54
|
+
|
|
55
|
+
elif self.path == "/update":
|
|
56
|
+
graceful_fail(self, body, self.server.node.handle_update)
|
|
57
|
+
|
|
58
|
+
elif self.path == "/ping":
|
|
59
|
+
respond_bytes(self, b'')
|
|
60
|
+
|
|
61
|
+
else:
|
|
62
|
+
self.send_response(404)
|
|
63
|
+
self.end_headers()
|
|
64
|
+
|
|
65
|
+
def log_message(self, format, *args):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def serve(httpd):
|
|
69
|
+
httpd.serve_forever()
|
|
70
|
+
|
|
71
|
+
class DSNodeServer(HTTPServer):
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
config: DSNodeConfig
|
|
75
|
+
):
|
|
76
|
+
super().__init__(("127.0.0.1", config.port), DSNodeHandler)
|
|
77
|
+
self.config = config
|
|
78
|
+
self.node = DSNode(config, VERSION)
|
|
79
|
+
self.node.logger.info(f'Started DSNode on port {config.port}')
|
|
80
|
+
|
|
81
|
+
def stop(self):
|
|
82
|
+
self.node.shutting_down = True
|
|
83
|
+
self.shutdown()
|
|
84
|
+
self.socket.close()
|
|
85
|
+
stop_thread(self.thread)
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def generate_key(out_file_path: str):
|
|
89
|
+
key = generate_aes_key()
|
|
90
|
+
with open(out_file_path, 'wb') as f:
|
|
91
|
+
f.write(key)
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def start(config: DSNodeConfig) -> 'NodeServer':
|
|
95
|
+
n = DSNodeServer(config)
|
|
96
|
+
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
97
|
+
public_path = n.node.cert_manager.public_path(n.config.node_id)
|
|
98
|
+
ssl_context.load_cert_chain(
|
|
99
|
+
certfile=public_path,
|
|
100
|
+
keyfile=public_path.replace(".crt", ".key")
|
|
101
|
+
)
|
|
102
|
+
n.socket = ssl_context.wrap_socket(n.socket, server_side=True)
|
|
103
|
+
n.thread = threading.Thread(target=serve, args=(n, ))
|
|
104
|
+
n.thread.start()
|
|
105
|
+
|
|
106
|
+
if n.config.bootstrap_nodes is not None and len(n.config.bootstrap_nodes) > 0:
|
|
107
|
+
for bs in n.config.bootstrap_nodes:
|
|
108
|
+
try:
|
|
109
|
+
n.node.bootstrap(bs)
|
|
110
|
+
break # Throws exception if connection is not made
|
|
111
|
+
except Exception as e:
|
|
112
|
+
print(e)
|
|
113
|
+
|
|
114
|
+
return n
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Dict, List
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from distributed_state_network.objects.endpoint import Endpoint
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class DSNodeConfig:
|
|
8
|
+
node_id: str
|
|
9
|
+
port: int
|
|
10
|
+
aes_key_file: str
|
|
11
|
+
bootstrap_nodes: List[Endpoint]
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def from_dict(data: Dict) -> 'DSNodeConfig':
|
|
15
|
+
return DSNodeConfig(data["node_id"], data["port"], data["aes_key_file"], [Endpoint.from_json(e) for e in data["bootstrap_nodes"]])
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from distributed_state_network.util.byte_helper import ByteHelper
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class Endpoint:
|
|
7
|
+
address: str
|
|
8
|
+
port: int
|
|
9
|
+
|
|
10
|
+
def to_string(self):
|
|
11
|
+
return f"{self.address}:{self.port}"
|
|
12
|
+
|
|
13
|
+
def to_bytes(self):
|
|
14
|
+
bts = ByteHelper()
|
|
15
|
+
bts.write_string(self.address)
|
|
16
|
+
bts.write_int(self.port)
|
|
17
|
+
|
|
18
|
+
return bts.get_bytes()
|
|
19
|
+
|
|
20
|
+
def to_json(self):
|
|
21
|
+
return {
|
|
22
|
+
"address": self.address,
|
|
23
|
+
"port": self.port
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def from_bytes(data: bytes):
|
|
28
|
+
bts = ByteHelper(data)
|
|
29
|
+
address = bts.read_string()
|
|
30
|
+
port = bts.read_int()
|
|
31
|
+
|
|
32
|
+
return Endpoint(address, port)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def from_json(data: Dict) -> 'Endpoint':
|
|
36
|
+
return Endpoint(data['address'], data['port'])
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
from distributed_state_network.objects.endpoint import Endpoint
|
|
6
|
+
|
|
7
|
+
from distributed_state_network.objects.signed_packet import SignedPacket
|
|
8
|
+
from distributed_state_network.util.byte_helper import ByteHelper
|
|
9
|
+
from distributed_state_network.util import bytes_to_int, int_to_bytes
|
|
10
|
+
|
|
11
|
+
class HelloPacket(SignedPacket):
|
|
12
|
+
version: str
|
|
13
|
+
node_id: str
|
|
14
|
+
connection: Endpoint
|
|
15
|
+
ecdsa_public_key: bytes
|
|
16
|
+
https_certificate: bytes
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
version: str,
|
|
21
|
+
node_id: str,
|
|
22
|
+
connection: Endpoint,
|
|
23
|
+
ecdsa_public_key: bytes,
|
|
24
|
+
ecdsa_signature: bytes,
|
|
25
|
+
https_certificate: bytes
|
|
26
|
+
):
|
|
27
|
+
super().__init__(ecdsa_signature)
|
|
28
|
+
self.version = version
|
|
29
|
+
self.node_id = node_id
|
|
30
|
+
self.connection = connection
|
|
31
|
+
self.ecdsa_public_key = ecdsa_public_key
|
|
32
|
+
self.https_certificate = https_certificate
|
|
33
|
+
|
|
34
|
+
def to_bytes(self, include_signature: bool = True):
|
|
35
|
+
bts = ByteHelper()
|
|
36
|
+
bts.write_string(self.version)
|
|
37
|
+
bts.write_string(self.node_id)
|
|
38
|
+
bts.write_bytes(self.connection.to_bytes())
|
|
39
|
+
bts.write_bytes(self.ecdsa_public_key)
|
|
40
|
+
bts.write_bytes(self.https_certificate)
|
|
41
|
+
if include_signature:
|
|
42
|
+
bts.write_bytes(self.ecdsa_signature)
|
|
43
|
+
|
|
44
|
+
return bts.get_bytes()
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def from_bytes(data: bytes):
|
|
48
|
+
bts = ByteHelper(data)
|
|
49
|
+
version = bts.read_string()
|
|
50
|
+
node_id = bts.read_string()
|
|
51
|
+
connection = Endpoint.from_bytes(bts.read_bytes())
|
|
52
|
+
ecdsa_public_key = bts.read_bytes()
|
|
53
|
+
https_certificate = bts.read_bytes()
|
|
54
|
+
ecdsa_signature = bts.read_bytes()
|
|
55
|
+
|
|
56
|
+
if version == '' or node_id == '' or ecdsa_public_key == b'' or https_certificate == b'':
|
|
57
|
+
raise Exception(406) # Not acceptable
|
|
58
|
+
|
|
59
|
+
return HelloPacket(version, node_id, connection, ecdsa_public_key, ecdsa_signature, https_certificate)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from distributed_state_network.objects.endpoint import Endpoint
|
|
4
|
+
from distributed_state_network.util.byte_helper import ByteHelper
|
|
5
|
+
from distributed_state_network.objects.signed_packet import SignedPacket
|
|
6
|
+
|
|
7
|
+
class PeersPacket(SignedPacket):
|
|
8
|
+
node_id: str
|
|
9
|
+
connections: Dict[str, Endpoint]
|
|
10
|
+
|
|
11
|
+
def __init__(self, node_id: str, ecdsa_signature: bytes, connections: Dict[str, Endpoint]):
|
|
12
|
+
super().__init__(ecdsa_signature)
|
|
13
|
+
self.node_id = node_id
|
|
14
|
+
self.connections = connections
|
|
15
|
+
|
|
16
|
+
def to_bytes(self, include_signature: bool = True) -> bytes:
|
|
17
|
+
bts = ByteHelper()
|
|
18
|
+
bts.write_string(self.node_id)
|
|
19
|
+
if include_signature:
|
|
20
|
+
bts.write_bytes(self.ecdsa_signature)
|
|
21
|
+
|
|
22
|
+
bts.write_int(len(self.connections.keys()))
|
|
23
|
+
for key in self.connections.keys():
|
|
24
|
+
bts.write_string(key)
|
|
25
|
+
bts.write_bytes(self.connections[key].to_bytes())
|
|
26
|
+
|
|
27
|
+
return bts.get_bytes()
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def from_bytes(data: bytes):
|
|
31
|
+
bts = ByteHelper(data)
|
|
32
|
+
node_id = bts.read_string()
|
|
33
|
+
ecdsa_signature = bts.read_bytes()
|
|
34
|
+
|
|
35
|
+
connections = { }
|
|
36
|
+
num_keys = bts.read_int()
|
|
37
|
+
for _ in range(num_keys):
|
|
38
|
+
key = bts.read_string()
|
|
39
|
+
connections[key] = Endpoint.from_bytes(bts.read_bytes())
|
|
40
|
+
|
|
41
|
+
return PeersPacket(node_id, ecdsa_signature, connections)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from distributed_state_network.util.ecdsa import verify_signature, sign_message
|
|
2
|
+
|
|
3
|
+
class SignedPacket:
|
|
4
|
+
ecdsa_signature: bytes
|
|
5
|
+
|
|
6
|
+
def __init__(self, ecdsa_signature: bytes):
|
|
7
|
+
self.ecdsa_signature = ecdsa_signature
|
|
8
|
+
|
|
9
|
+
def sign(self, private_key: bytes):
|
|
10
|
+
self.ecdsa_signature = sign_message(private_key, self.to_bytes(False))
|
|
11
|
+
|
|
12
|
+
def verify_signature(self, public_key: bytes):
|
|
13
|
+
return verify_signature(public_key, self.to_bytes(False), self.ecdsa_signature)
|
|
14
|
+
|
|
15
|
+
def to_bytes(include_signature: bool = True):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def from_bytes():
|
|
20
|
+
pass
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import json
|
|
3
|
+
from typing import Dict, Tuple
|
|
4
|
+
|
|
5
|
+
from distributed_state_network.objects.signed_packet import SignedPacket
|
|
6
|
+
from distributed_state_network.util.byte_helper import ByteHelper
|
|
7
|
+
|
|
8
|
+
class StatePacket(SignedPacket):
|
|
9
|
+
node_id: str
|
|
10
|
+
state_data: Dict[str, str]
|
|
11
|
+
last_update: float
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
node_id: str,
|
|
16
|
+
last_update: float,
|
|
17
|
+
ecdsa_signature: bytes,
|
|
18
|
+
state_data: Dict[str, str]
|
|
19
|
+
):
|
|
20
|
+
super().__init__(ecdsa_signature)
|
|
21
|
+
self.node_id = node_id
|
|
22
|
+
self.state_data = state_data
|
|
23
|
+
self.last_update = last_update
|
|
24
|
+
|
|
25
|
+
def update_state(self, key: str, val: str, private_key: bytes):
|
|
26
|
+
self.state_data[key] = val
|
|
27
|
+
self.last_update = time.time()
|
|
28
|
+
self.sign(private_key)
|
|
29
|
+
|
|
30
|
+
def to_bytes(self, include_signature: bool = True):
|
|
31
|
+
bts = ByteHelper()
|
|
32
|
+
bts.write_string(self.node_id)
|
|
33
|
+
bts.write_float(self.last_update)
|
|
34
|
+
if include_signature:
|
|
35
|
+
bts.write_bytes(self.ecdsa_signature)
|
|
36
|
+
bts.write_string(json.dumps(self.state_data))
|
|
37
|
+
|
|
38
|
+
return bts.get_bytes()
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def create(
|
|
42
|
+
node_id: str,
|
|
43
|
+
last_update: float,
|
|
44
|
+
ecdsa_private_key: bytes,
|
|
45
|
+
state_data: Dict[str, str]
|
|
46
|
+
):
|
|
47
|
+
s = StatePacket(node_id, last_update, b'', state_data)
|
|
48
|
+
s.sign(ecdsa_private_key)
|
|
49
|
+
return s
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def from_bytes(data: bytes):
|
|
54
|
+
bts = ByteHelper(data)
|
|
55
|
+
node_id = bts.read_string()
|
|
56
|
+
|
|
57
|
+
if node_id == '':
|
|
58
|
+
raise Exception(406) # Not acceptable
|
|
59
|
+
|
|
60
|
+
last_update = bts.read_float()
|
|
61
|
+
ecdsa_signature = bts.read_bytes()
|
|
62
|
+
state_data = json.loads(bts.read_string())
|
|
63
|
+
|
|
64
|
+
return StatePacket(node_id, last_update, ecdsa_signature, state_data)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import ctypes
|
|
3
|
+
import struct
|
|
4
|
+
import threading
|
|
5
|
+
from hashlib import sha256
|
|
6
|
+
|
|
7
|
+
def int_to_bytes(i: int) -> bytes:
|
|
8
|
+
return i.to_bytes(4, 'little', signed=False)
|
|
9
|
+
|
|
10
|
+
def bytes_to_int(b: bytes) -> int:
|
|
11
|
+
return int.from_bytes(b, 'little', signed=False)
|
|
12
|
+
|
|
13
|
+
def float_to_bytes(f: float) -> bytes:
|
|
14
|
+
return struct.pack(">d", f)
|
|
15
|
+
|
|
16
|
+
def bytes_to_float(b: bytes) -> float:
|
|
17
|
+
return struct.unpack(">d", b)[0]
|
|
18
|
+
|
|
19
|
+
def get_byte_hash(data: bytes) -> bytes:
|
|
20
|
+
return sha256(data).digest()
|
|
21
|
+
|
|
22
|
+
def get_hash(data: str) -> bytes:
|
|
23
|
+
return get_byte_hash(data.encode('utf-8'))
|
|
24
|
+
|
|
25
|
+
def get_dict_hash(data: dict) -> bytes:
|
|
26
|
+
return get_hash(json.dumps(data))
|
|
27
|
+
|
|
28
|
+
def stop_thread(thread: threading.Thread):
|
|
29
|
+
thread_id = thread.ident
|
|
30
|
+
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id,
|
|
31
|
+
ctypes.py_object(SystemExit))
|
|
32
|
+
if res > 1: # pragma: no cover
|
|
33
|
+
ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0)
|
|
34
|
+
print('Exception raise failure')
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
|
|
4
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
5
|
+
from cryptography.hazmat.backends import default_backend
|
|
6
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
7
|
+
from cryptography.hazmat.primitives import hashes, padding as sym_padding
|
|
8
|
+
|
|
9
|
+
def get_cipher(key: bytes, iv: bytes) -> Cipher:
|
|
10
|
+
return Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
|
11
|
+
|
|
12
|
+
def get_iv() -> bytes:
|
|
13
|
+
return os.urandom(16)
|
|
14
|
+
|
|
15
|
+
def generate_aes_key() -> bytes:
|
|
16
|
+
key = PBKDF2HMAC(
|
|
17
|
+
algorithm=hashes.SHA256(),
|
|
18
|
+
length=16,
|
|
19
|
+
salt=os.urandom(16),
|
|
20
|
+
iterations=100000,
|
|
21
|
+
backend=default_backend()
|
|
22
|
+
).derive(os.urandom(128))
|
|
23
|
+
iv = get_iv()
|
|
24
|
+
bts = BytesIO()
|
|
25
|
+
bts.write(iv)
|
|
26
|
+
bts.write(key)
|
|
27
|
+
return bts.getvalue()
|
|
28
|
+
|
|
29
|
+
def aes_decrypt(key: bytes, ciphertext: bytes) -> bytes:
|
|
30
|
+
bts = BytesIO(key)
|
|
31
|
+
iv = bts.read(16)
|
|
32
|
+
aes_key = bts.read()
|
|
33
|
+
decryptor = get_cipher(aes_key, iv).decryptor()
|
|
34
|
+
unpadder = sym_padding.PKCS7(128).unpadder()
|
|
35
|
+
decrypted_text = decryptor.update(ciphertext) + decryptor.finalize()
|
|
36
|
+
return unpadder.update(decrypted_text) + unpadder.finalize()
|
|
37
|
+
|
|
38
|
+
def aes_encrypt(key: bytes, data: bytes) -> bytes:
|
|
39
|
+
bts = BytesIO(key)
|
|
40
|
+
iv = bts.read(16)
|
|
41
|
+
aes_key = bts.read()
|
|
42
|
+
encryptor = get_cipher(aes_key, iv).encryptor()
|
|
43
|
+
padder = sym_padding.PKCS7(128).padder()
|
|
44
|
+
padded_data = padder.update(data) + padder.finalize()
|
|
45
|
+
return encryptor.update(padded_data) + encryptor.finalize()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from io import BytesIO
|
|
2
|
+
from distributed_state_network.util import bytes_to_int, int_to_bytes, float_to_bytes, bytes_to_float
|
|
3
|
+
|
|
4
|
+
class ByteHelper:
|
|
5
|
+
def __init__(self, data: bytes = None):
|
|
6
|
+
self.bts = BytesIO(data)
|
|
7
|
+
|
|
8
|
+
def write_string(self, s: str):
|
|
9
|
+
encoded = s.encode('utf-8')
|
|
10
|
+
self.write_bytes(encoded)
|
|
11
|
+
|
|
12
|
+
def write_int(self, i: int):
|
|
13
|
+
self.bts.write(int_to_bytes(i))
|
|
14
|
+
|
|
15
|
+
def write_float(self, f: float):
|
|
16
|
+
self.bts.write(float_to_bytes(f))
|
|
17
|
+
|
|
18
|
+
def write_bytes(self, b: bytes):
|
|
19
|
+
self.bts.write(int_to_bytes(len(b)))
|
|
20
|
+
self.bts.write(b)
|
|
21
|
+
|
|
22
|
+
def read_string(self):
|
|
23
|
+
return self.read_bytes().decode('utf-8')
|
|
24
|
+
|
|
25
|
+
def read_int(self):
|
|
26
|
+
return bytes_to_int(self.bts.read(4))
|
|
27
|
+
|
|
28
|
+
def read_float(self):
|
|
29
|
+
return bytes_to_float(self.bts.read(8))
|
|
30
|
+
|
|
31
|
+
def read_bytes(self):
|
|
32
|
+
l = bytes_to_int(self.bts.read(4))
|
|
33
|
+
return self.bts.read(l)
|
|
34
|
+
|
|
35
|
+
def get_bytes(self):
|
|
36
|
+
return self.bts.getvalue()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
|
|
3
|
+
from ecdsa import SigningKey, VerifyingKey, SECP256k1
|
|
4
|
+
|
|
5
|
+
def generate_key_pair():
|
|
6
|
+
private_key = SigningKey.generate(curve=SECP256k1)
|
|
7
|
+
public_key = private_key.get_verifying_key()
|
|
8
|
+
return public_key.to_string(), private_key.to_string()
|
|
9
|
+
|
|
10
|
+
def sign_message(private_key: bytes, message: bytes) -> bytes:
|
|
11
|
+
private_key = SigningKey.from_string(private_key, curve=SECP256k1)
|
|
12
|
+
message_hash = hashlib.sha256(message).digest()
|
|
13
|
+
return private_key.sign(message_hash)
|
|
14
|
+
|
|
15
|
+
def verify_signature(public_key: bytes, message: bytes, signature: bytes):
|
|
16
|
+
public_key_obj = VerifyingKey.from_string(public_key, curve=SECP256k1)
|
|
17
|
+
message_hash = hashlib.sha256(message).digest()
|
|
18
|
+
try:
|
|
19
|
+
return public_key_obj.verify(signature, message_hash)
|
|
20
|
+
except Exception:
|
|
21
|
+
return False
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import ipaddress
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
|
|
4
|
+
from cryptography import x509
|
|
5
|
+
from cryptography.x509 import IPAddress
|
|
6
|
+
from cryptography.x509.oid import NameOID
|
|
7
|
+
from cryptography.hazmat.primitives import hashes
|
|
8
|
+
from cryptography.hazmat.backends import default_backend
|
|
9
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
10
|
+
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption
|
|
11
|
+
|
|
12
|
+
def generate_cert():
|
|
13
|
+
# Generate private key
|
|
14
|
+
private_key = rsa.generate_private_key(
|
|
15
|
+
public_exponent=65537,
|
|
16
|
+
key_size=2048,
|
|
17
|
+
backend=default_backend()
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Generate a self-signed certificate
|
|
21
|
+
subject = issuer = x509.Name([
|
|
22
|
+
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
|
|
23
|
+
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"),
|
|
24
|
+
x509.NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"),
|
|
25
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "My Organization"),
|
|
26
|
+
x509.NameAttribute(NameOID.COMMON_NAME, "localhost"),
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
certificate = (
|
|
30
|
+
x509.CertificateBuilder()
|
|
31
|
+
.subject_name(subject)
|
|
32
|
+
.issuer_name(issuer)
|
|
33
|
+
.public_key(private_key.public_key())
|
|
34
|
+
.serial_number(x509.random_serial_number())
|
|
35
|
+
.not_valid_before(datetime.utcnow())
|
|
36
|
+
.not_valid_after(datetime.utcnow() + timedelta(days=365))
|
|
37
|
+
.add_extension(
|
|
38
|
+
x509.SubjectAlternativeName([
|
|
39
|
+
x509.DNSName("localhost"),
|
|
40
|
+
IPAddress(ipaddress.IPv4Address("127.0.0.1"))
|
|
41
|
+
]),
|
|
42
|
+
critical=False
|
|
43
|
+
)
|
|
44
|
+
.sign(private_key, hashes.SHA256(), default_backend())
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
cert_bytes = certificate.public_bytes(Encoding.PEM)
|
|
48
|
+
private_key_bytes = private_key.private_bytes(
|
|
49
|
+
encoding=Encoding.PEM,
|
|
50
|
+
format=PrivateFormat.TraditionalOpenSSL,
|
|
51
|
+
encryption_algorithm=NoEncryption()
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return cert_bytes, private_key_bytes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Callable, Tuple
|
|
4
|
+
|
|
5
|
+
from distributed_state_network.util.ecdsa import generate_key_pair
|
|
6
|
+
from distributed_state_network.util.https import generate_cert
|
|
7
|
+
|
|
8
|
+
class KeyManager:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
key_type: str,
|
|
12
|
+
node_id: str,
|
|
13
|
+
folder: str,
|
|
14
|
+
public_extension: str,
|
|
15
|
+
private_extension: str,
|
|
16
|
+
gen_keys: Callable[[], Tuple[bytes, bytes]]
|
|
17
|
+
):
|
|
18
|
+
self.key_type = key_type
|
|
19
|
+
self.node_id = node_id
|
|
20
|
+
self.folder = folder
|
|
21
|
+
self.public_extension = public_extension
|
|
22
|
+
self.private_extension = private_extension
|
|
23
|
+
self.gen_keys = gen_keys
|
|
24
|
+
|
|
25
|
+
def write_public(self, node_id: str, cert: bytes):
|
|
26
|
+
if not os.path.exists(self.folder):
|
|
27
|
+
os.mkdir(self.folder)
|
|
28
|
+
if not os.path.exists(f'{self.folder}/{node_id}'):
|
|
29
|
+
os.mkdir(f'{self.folder}/{node_id}')
|
|
30
|
+
with open(f'{self.folder}/{self.node_id}/{node_id}.{self.public_extension}', 'wb') as f:
|
|
31
|
+
f.write(cert)
|
|
32
|
+
|
|
33
|
+
def read_public(self, node_id: str) -> bytes:
|
|
34
|
+
if not os.path.exists(f'{self.folder}/{self.node_id}/{node_id}.{self.public_extension}'):
|
|
35
|
+
raise Exception(401)
|
|
36
|
+
with open(f'{self.folder}/{self.node_id}/{node_id}.{self.public_extension}', 'rb') as f:
|
|
37
|
+
return f.read()
|
|
38
|
+
|
|
39
|
+
def has_public(self, node_id: str) -> bool:
|
|
40
|
+
return os.path.exists(f'{self.folder}/{self.node_id}/{node_id}.{self.public_extension}')
|
|
41
|
+
|
|
42
|
+
def ensure_public(self, node_id: str, public_key: bytes):
|
|
43
|
+
if self.has_public(node_id):
|
|
44
|
+
if not self.verify_public(node_id, public_key):
|
|
45
|
+
raise Exception(401)
|
|
46
|
+
else:
|
|
47
|
+
self.write_public(node_id, public_key)
|
|
48
|
+
|
|
49
|
+
def public_path(self, node_id: str):
|
|
50
|
+
return f"{self.folder}/{self.node_id}/{node_id}.{self.public_extension}"
|
|
51
|
+
|
|
52
|
+
def verify_public(self, node_id: str, public_key: bytes):
|
|
53
|
+
if not self.has_public(node_id):
|
|
54
|
+
return False
|
|
55
|
+
return public_key == self.read_public(node_id)
|
|
56
|
+
|
|
57
|
+
def my_public(self) -> bytes:
|
|
58
|
+
return self.read_public(self.node_id)
|
|
59
|
+
|
|
60
|
+
def my_private(self):
|
|
61
|
+
if not os.path.exists(f'{self.folder}/{self.node_id}/{self.node_id}.{self.private_extension}'):
|
|
62
|
+
raise Exception("Private key not found")
|
|
63
|
+
with open(f'{self.folder}/{self.node_id}/{self.node_id}.{self.private_extension}', 'rb') as f:
|
|
64
|
+
return f.read()
|
|
65
|
+
|
|
66
|
+
def generate_keys(self):
|
|
67
|
+
if os.path.exists(f'{self.folder}/{self.node_id}/{self.node_id}.{self.private_extension}'):
|
|
68
|
+
return
|
|
69
|
+
logging.getLogger("DSN: " + self.node_id).info(f"Generating {self.key_type} Keys ...")
|
|
70
|
+
cert_bytes, key_bytes = self.gen_keys()
|
|
71
|
+
if not os.path.exists(self.folder):
|
|
72
|
+
os.mkdir(self.folder)
|
|
73
|
+
if not os.path.exists(f'{self.folder}/{self.node_id}'):
|
|
74
|
+
os.mkdir(f'{self.folder}/{self.node_id}')
|
|
75
|
+
with open(f'{self.folder}/{self.node_id}/{self.node_id}.{self.public_extension}', 'wb') as f:
|
|
76
|
+
f.write(cert_bytes)
|
|
77
|
+
with open(f'{self.folder}/{self.node_id}/{self.node_id}.{self.private_extension}', 'wb') as f:
|
|
78
|
+
f.write(key_bytes)
|
|
79
|
+
|
|
80
|
+
class CertManager(KeyManager):
|
|
81
|
+
def __init__(self, node_id: str):
|
|
82
|
+
KeyManager.__init__(self, "HTTPS", node_id, "certs", "crt", "key", generate_cert)
|
|
83
|
+
|
|
84
|
+
class CredentialManager(KeyManager):
|
|
85
|
+
def __init__(self, node_id: str):
|
|
86
|
+
KeyManager.__init__(self, "ECDSA", node_id, "credentials", "pub", "key", generate_key_pair)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: distributed-state-network
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A tool to distribute the state of a network device to other devices on the network
|
|
5
|
+
Project-URL: Homepage, https://github.com/erinclemmer/distributed_state_network
|
|
6
|
+
Project-URL: Issues, https://github.com/erinclemmer/distributed_state_network/issues
|
|
7
|
+
Author-email: Erin Clemmer <erin.c.clemmer@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: distributed,networking
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Telecommunications Industry
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Topic :: Communications
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: cryptography
|
|
20
|
+
Requires-Dist: ecdsa
|
|
21
|
+
Requires-Dist: ipaddress
|
|
22
|
+
Requires-Dist: logging
|
|
23
|
+
Requires-Dist: requests
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Distributed State Network
|
|
27
|
+
|
|
28
|
+
A Python framework for building distributed applications where nodes automatically share state without explicit data requests.
|
|
29
|
+
|
|
30
|
+
## Why DSN?
|
|
31
|
+
|
|
32
|
+
Traditional distributed systems require constant polling or complex pub/sub mechanisms to share state between nodes. DSN solves this by providing:
|
|
33
|
+
|
|
34
|
+
- **Automatic state synchronization** - Changes propagate instantly across the network
|
|
35
|
+
- **No single point of failure** - Every node maintains its own state
|
|
36
|
+
- **Simple key-value interface** - Read any node's data as easily as local variables
|
|
37
|
+
- **Complete Security** - Triple-layer encryption protects your network
|
|
38
|
+
|
|
39
|
+
Perfect for building distributed monitoring systems, IoT networks, or any application where multiple machines need to share state efficiently.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install distributed-state-network
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
### 1. Create Your First Node
|
|
50
|
+
|
|
51
|
+
The simplest DSN network is a single node:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from distributed_state_network import DSNodeServer, DSNodeConfig
|
|
55
|
+
|
|
56
|
+
# Generate a network key (do this once for your entire network)
|
|
57
|
+
DSNodeServer.generate_key("network.key")
|
|
58
|
+
|
|
59
|
+
# Start a node
|
|
60
|
+
node = DSNodeServer.start(DSNodeConfig(
|
|
61
|
+
node_id="my_first_node",
|
|
62
|
+
port=8000,
|
|
63
|
+
aes_key_file="network.key",
|
|
64
|
+
bootstrap_nodes=[] # Empty for the first node
|
|
65
|
+
))
|
|
66
|
+
|
|
67
|
+
# Write some data
|
|
68
|
+
node.node.update_data("status", "online")
|
|
69
|
+
node.node.update_data("temperature", "72.5")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## How It Works
|
|
73
|
+
|
|
74
|
+
DSN creates a peer-to-peer network where each node maintains its own state database:
|
|
75
|
+
|
|
76
|
+
**Key concepts:**
|
|
77
|
+
- Each node owns its state and is the only one who can modify it
|
|
78
|
+
- State changes are automatically broadcast to all connected nodes
|
|
79
|
+
- Any node can read any other node's state instantly
|
|
80
|
+
- All communication is encrypted with AES + ECDSA + HTTPS
|
|
81
|
+
|
|
82
|
+
## API Reference
|
|
83
|
+
|
|
84
|
+
### Node Methods
|
|
85
|
+
|
|
86
|
+
**update_data(key, value)** - Update a key in this node's state
|
|
87
|
+
```python
|
|
88
|
+
node.update_data("sensor_reading", "42.0")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**read_data(node_id, key)** - Read a value from any node's state
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
temperature = node.read_data("sensor_node", "temperature")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**peers()** - List all connected nodes
|
|
98
|
+
```python
|
|
99
|
+
connected_nodes = node.peers()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Real-World Examples
|
|
103
|
+
|
|
104
|
+
### Distributed Temperature Monitoring
|
|
105
|
+
|
|
106
|
+
Create a network of temperature sensors that share readings:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# On each Raspberry Pi with a sensor:
|
|
110
|
+
sensor_node = DSNodeServer.start(DSNodeConfig(
|
|
111
|
+
node_id=f"sensor_{location}",
|
|
112
|
+
port=8000,
|
|
113
|
+
aes_key_file="network.key",
|
|
114
|
+
bootstrap_nodes=[{"address": "coordinator.local", "port": 8000}]
|
|
115
|
+
))
|
|
116
|
+
|
|
117
|
+
# Continuously update temperature
|
|
118
|
+
while True:
|
|
119
|
+
temp = read_temperature_sensor()
|
|
120
|
+
sensor_node.node.update_data("temperature", str(temp))
|
|
121
|
+
sensor_node.node.update_data("timestamp", str(time.time()))
|
|
122
|
+
time.sleep(60)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
On the monitoring station:
|
|
126
|
+
```python
|
|
127
|
+
for node_id in monitor.node.peers():
|
|
128
|
+
if node_id.startswith("sensor_"):
|
|
129
|
+
temp = monitor.node.read_data(node_id, "temperature")
|
|
130
|
+
print(f"{node_id}: {temp}°F")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Troubleshooting
|
|
134
|
+
|
|
135
|
+
### Node Can't Connect
|
|
136
|
+
- Verify the AES key file is identical on all nodes
|
|
137
|
+
- Check firewall rules allow traffic on the configured port
|
|
138
|
+
- Ensure bootstrap node address is reachable
|
|
139
|
+
|
|
140
|
+
### State Not Updating
|
|
141
|
+
- Confirm nodes show as connected with `node.peers()`
|
|
142
|
+
- Check network latency between nodes
|
|
143
|
+
- Verify no duplicate node IDs (each must be unique)
|
|
144
|
+
|
|
145
|
+
## Performance Characteristics
|
|
146
|
+
- State updates typically propagate in <100ms on local networks
|
|
147
|
+
- Each node stores the complete state of all other nodes
|
|
148
|
+
- Suitable for networks up to ~100 nodes
|
|
149
|
+
- State values should be kept reasonably small (< 1MB per key)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
distributed_state_network/__init__.py,sha256=vqniZquF5Z9P3extp_DyXyyskSg9k3tSLq2Huky7dmg,240
|
|
2
|
+
distributed_state_network/create_key.py,sha256=fZcAy0AgQza3R7HWLgyNHq_yRo28mLFvB-6jdNdm5jc,247
|
|
3
|
+
distributed_state_network/dsnode.py,sha256=n45PlVCxqyN-nD42GBt-dNr7il6CBZL5oTaZnjA7DrI,10756
|
|
4
|
+
distributed_state_network/handler.py,sha256=61gMWN3nHwtEghvn9XArzcmdduYPLOMjXQFtngMRcBI,3823
|
|
5
|
+
distributed_state_network/objects/config.py,sha256=p9sKKam2dcRiLuAgJ4KkGsXxbjec2EGQbqNHK3Cu548,485
|
|
6
|
+
distributed_state_network/objects/endpoint.py,sha256=odDAc9nYFtViVYF68zgmJIN1f9NF5bGAIo66YyvOzso,885
|
|
7
|
+
distributed_state_network/objects/hello_packet.py,sha256=oWUehRFRy6sYdxbKMaX_4YHaZFS7S8VxKm7V1f0BWJw,2033
|
|
8
|
+
distributed_state_network/objects/peers_packet.py,sha256=sNAJ32ViW5KD_45XW8hPi_KqU7lfPYwXU0DRgU8bFao,1414
|
|
9
|
+
distributed_state_network/objects/signed_packet.py,sha256=NnpfdJVk1fgeVSqnGYRzDMwYmTwQgqn3XDrq3k_0mwg,601
|
|
10
|
+
distributed_state_network/objects/state_packet.py,sha256=-tlhEh-0sNN1EGmbGxXwBrcmDCOmsLTFWkGVRuF-d-Y,1904
|
|
11
|
+
distributed_state_network/util/__init__.py,sha256=aV1nFoFsIxVfT8qXhYevJx9GQ7hth5bbpt59xT6qk2I,995
|
|
12
|
+
distributed_state_network/util/aes.py,sha256=CYMw2C4wyTdTyo3p2rq4Q2fLkvDobOLYBxW-_6dYSsQ,1572
|
|
13
|
+
distributed_state_network/util/byte_helper.py,sha256=dAhJW_gIXlJYoWnC9WSO3i8b0zgvofpKUa1vrr9IPhQ,976
|
|
14
|
+
distributed_state_network/util/ecdsa.py,sha256=-u9VtEEcx4TLNotIhM7m2uweXA92eznsW0EVFgY9s6M,804
|
|
15
|
+
distributed_state_network/util/https.py,sha256=k2eNOHQih_-6NPARpBnfpDCPDf1TFtXG3LiIgIMHsxU,1985
|
|
16
|
+
distributed_state_network/util/key_manager.py,sha256=5Ho10putWGglmeC24SEGqxAAaOfT0CMiCP99Toq0LfM,3616
|
|
17
|
+
distributed_state_network-0.0.1.dist-info/METADATA,sha256=dL_B9cNDiuO5JjVwH0KIZmg3efjzpdAzFQ_OBUUV__s,4577
|
|
18
|
+
distributed_state_network-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
19
|
+
distributed_state_network-0.0.1.dist-info/licenses/LICENSE,sha256=cfYX34vuMK24B499ZkFHRoVWarXM9-p0yyy0OrtSYdQ,1090
|
|
20
|
+
distributed_state_network-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Colton Clemmer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|