distributed-state-network 0.0.1__tar.gz

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.
Files changed (26) hide show
  1. distributed_state_network-0.0.1/.github/workflows/publish.yml +35 -0
  2. distributed_state_network-0.0.1/.gitignore +10 -0
  3. distributed_state_network-0.0.1/LICENSE +21 -0
  4. distributed_state_network-0.0.1/PKG-INFO +149 -0
  5. distributed_state_network-0.0.1/README.md +124 -0
  6. distributed_state_network-0.0.1/coverage.sh +3 -0
  7. distributed_state_network-0.0.1/pyproject.toml +37 -0
  8. distributed_state_network-0.0.1/security.md +14 -0
  9. distributed_state_network-0.0.1/src/distributed_state_network/__init__.py +4 -0
  10. distributed_state_network-0.0.1/src/distributed_state_network/create_key.py +10 -0
  11. distributed_state_network-0.0.1/src/distributed_state_network/dsnode.py +263 -0
  12. distributed_state_network-0.0.1/src/distributed_state_network/handler.py +114 -0
  13. distributed_state_network-0.0.1/src/distributed_state_network/objects/config.py +15 -0
  14. distributed_state_network-0.0.1/src/distributed_state_network/objects/endpoint.py +36 -0
  15. distributed_state_network-0.0.1/src/distributed_state_network/objects/hello_packet.py +59 -0
  16. distributed_state_network-0.0.1/src/distributed_state_network/objects/peers_packet.py +41 -0
  17. distributed_state_network-0.0.1/src/distributed_state_network/objects/signed_packet.py +20 -0
  18. distributed_state_network-0.0.1/src/distributed_state_network/objects/state_packet.py +64 -0
  19. distributed_state_network-0.0.1/src/distributed_state_network/util/__init__.py +34 -0
  20. distributed_state_network-0.0.1/src/distributed_state_network/util/aes.py +45 -0
  21. distributed_state_network-0.0.1/src/distributed_state_network/util/byte_helper.py +36 -0
  22. distributed_state_network-0.0.1/src/distributed_state_network/util/ecdsa.py +21 -0
  23. distributed_state_network-0.0.1/src/distributed_state_network/util/https.py +54 -0
  24. distributed_state_network-0.0.1/src/distributed_state_network/util/key_manager.py +86 -0
  25. distributed_state_network-0.0.1/technical.md +95 -0
  26. distributed_state_network-0.0.1/test.py +366 -0
@@ -0,0 +1,35 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ deploy:
12
+
13
+ runs-on: ubuntu-latest
14
+
15
+ environment: release
16
+ permissions:
17
+ id-token: write
18
+
19
+ steps:
20
+ - uses: actions/checkout@v3
21
+ - name: Set up Python
22
+ uses: actions/setup-python@v4
23
+ with:
24
+ python-version: '3.10'
25
+ cache: 'pip'
26
+ - name: Install dependencies
27
+ run: |
28
+ python -m pip install --upgrade pip
29
+ pip install hatch
30
+ - name: Build package
31
+ run: hatch build
32
+ - name: Publish package distributions to PyPI
33
+ uses: pypa/gh-action-pypi-publish@release/v1
34
+ with:
35
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ env/
3
+ certs/
4
+ .vscode/
5
+ credentials/
6
+ test.key
7
+ .coverage
8
+ count.py
9
+ appended.py
10
+ py_line_count_history.jsonl
@@ -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.
@@ -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,124 @@
1
+ # Distributed State Network
2
+
3
+ A Python framework for building distributed applications where nodes automatically share state without explicit data requests.
4
+
5
+ ## Why DSN?
6
+
7
+ Traditional distributed systems require constant polling or complex pub/sub mechanisms to share state between nodes. DSN solves this by providing:
8
+
9
+ - **Automatic state synchronization** - Changes propagate instantly across the network
10
+ - **No single point of failure** - Every node maintains its own state
11
+ - **Simple key-value interface** - Read any node's data as easily as local variables
12
+ - **Complete Security** - Triple-layer encryption protects your network
13
+
14
+ Perfect for building distributed monitoring systems, IoT networks, or any application where multiple machines need to share state efficiently.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install distributed-state-network
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ### 1. Create Your First Node
25
+
26
+ The simplest DSN network is a single node:
27
+
28
+ ```python
29
+ from distributed_state_network import DSNodeServer, DSNodeConfig
30
+
31
+ # Generate a network key (do this once for your entire network)
32
+ DSNodeServer.generate_key("network.key")
33
+
34
+ # Start a node
35
+ node = DSNodeServer.start(DSNodeConfig(
36
+ node_id="my_first_node",
37
+ port=8000,
38
+ aes_key_file="network.key",
39
+ bootstrap_nodes=[] # Empty for the first node
40
+ ))
41
+
42
+ # Write some data
43
+ node.node.update_data("status", "online")
44
+ node.node.update_data("temperature", "72.5")
45
+ ```
46
+
47
+ ## How It Works
48
+
49
+ DSN creates a peer-to-peer network where each node maintains its own state database:
50
+
51
+ **Key concepts:**
52
+ - Each node owns its state and is the only one who can modify it
53
+ - State changes are automatically broadcast to all connected nodes
54
+ - Any node can read any other node's state instantly
55
+ - All communication is encrypted with AES + ECDSA + HTTPS
56
+
57
+ ## API Reference
58
+
59
+ ### Node Methods
60
+
61
+ **update_data(key, value)** - Update a key in this node's state
62
+ ```python
63
+ node.update_data("sensor_reading", "42.0")
64
+ ```
65
+
66
+ **read_data(node_id, key)** - Read a value from any node's state
67
+
68
+ ```python
69
+ temperature = node.read_data("sensor_node", "temperature")
70
+ ```
71
+
72
+ **peers()** - List all connected nodes
73
+ ```python
74
+ connected_nodes = node.peers()
75
+ ```
76
+
77
+ ## Real-World Examples
78
+
79
+ ### Distributed Temperature Monitoring
80
+
81
+ Create a network of temperature sensors that share readings:
82
+
83
+ ```python
84
+ # On each Raspberry Pi with a sensor:
85
+ sensor_node = DSNodeServer.start(DSNodeConfig(
86
+ node_id=f"sensor_{location}",
87
+ port=8000,
88
+ aes_key_file="network.key",
89
+ bootstrap_nodes=[{"address": "coordinator.local", "port": 8000}]
90
+ ))
91
+
92
+ # Continuously update temperature
93
+ while True:
94
+ temp = read_temperature_sensor()
95
+ sensor_node.node.update_data("temperature", str(temp))
96
+ sensor_node.node.update_data("timestamp", str(time.time()))
97
+ time.sleep(60)
98
+ ```
99
+
100
+ On the monitoring station:
101
+ ```python
102
+ for node_id in monitor.node.peers():
103
+ if node_id.startswith("sensor_"):
104
+ temp = monitor.node.read_data(node_id, "temperature")
105
+ print(f"{node_id}: {temp}°F")
106
+ ```
107
+
108
+ ## Troubleshooting
109
+
110
+ ### Node Can't Connect
111
+ - Verify the AES key file is identical on all nodes
112
+ - Check firewall rules allow traffic on the configured port
113
+ - Ensure bootstrap node address is reachable
114
+
115
+ ### State Not Updating
116
+ - Confirm nodes show as connected with `node.peers()`
117
+ - Check network latency between nodes
118
+ - Verify no duplicate node IDs (each must be unique)
119
+
120
+ ## Performance Characteristics
121
+ - State updates typically propagate in <100ms on local networks
122
+ - Each node stores the complete state of all other nodes
123
+ - Suitable for networks up to ~100 nodes
124
+ - State values should be kept reasonably small (< 1MB per key)
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ coverage run test.py && coverage report -m --omit=test.py
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["hatchling", "cryptography", "logging", "ipaddress", "requests", "ecdsa"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "distributed-state-network"
7
+ version = "0.0.1"
8
+ authors = [{name="Erin Clemmer", email="erin.c.clemmer@gmail.com"}]
9
+ description = "A tool to distribute the state of a network device to other devices on the network"
10
+ keywords = ["distributed", "networking"]
11
+ license = "MIT"
12
+ readme = "README.md"
13
+ license-files = ["LICENSE"]
14
+ classifiers = [
15
+ "Programming Language :: Python",
16
+ "Development Status :: 5 - Production/Stable",
17
+ "Operating System :: OS Independent",
18
+ "Intended Audience :: Telecommunications Industry",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Topic :: Communications",
21
+ "Typing :: Typed"
22
+ ]
23
+ requires-python = ">=3.10"
24
+ dependencies = [
25
+ "cryptography",
26
+ "logging",
27
+ "ipaddress",
28
+ "requests",
29
+ "ecdsa"
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/erinclemmer/distributed_state_network"
34
+ Issues = "https://github.com/erinclemmer/distributed_state_network/issues"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/distributed_state_network"]
@@ -0,0 +1,14 @@
1
+ ## Security Architecture
2
+ We use a three-layer security model where each layer addresses a specific vulnerability. Understanding this design helps explain why certain setup steps are necessary and how your data stays protected.
3
+ The Three-Layer Defense
4
+ Think of our security like protecting a secret club where members share sensitive information. You need multiple types of protection because different attacks require different defenses.
5
+ ### Layer 1: AES
6
+ AES Encryption acts as the network's membership card. Every message between nodes is encrypted with a shared AES key, making the network invisible to outsiders. If someone intercepts your network traffic without this key, they see only meaningless encrypted bytes. This is why you must carefully share the key file with trusted nodes - it's the fundamental gatekeeper of your network.
7
+ ### Layer 2: ECDSA
8
+ ECDSA Signatures solve a critical problem that AES alone cannot address. Once someone has the network key, how do you prevent them from impersonating other nodes? Each node has its own unforgeable cryptographic identity. When node "sensor_1" shares its temperature reading, it signs that data with its private key. Other nodes verify this signature to ensure the data truly came from sensor_1, not from an attacker pretending to be sensor_1. This is why node IDs must be unique - each ID is permanently bound to a specific cryptographic identity.
9
+ ### Layer 3: HTTPS
10
+ HTTPS/TLS prevents sophisticated network-level attacks. Even with encryption and signatures, an attacker positioned between two nodes could intercept messages, block updates, or redirect traffic to malicious servers. HTTPS certificates ensure you're communicating with the exact node you intend to reach. During the initial handshake, nodes exchange certificates that get verified on every subsequent connection, creating authenticated channels that can't be hijacked.
11
+
12
+ ## Why This Matters
13
+ Each layer compensates for what the others cannot protect. AES keeps outsiders out but can't distinguish between insiders. ECDSA proves identity but doesn't hide data. HTTPS secures the transport but doesn't control network access. Together, they create a system where only authorized nodes can join, every piece of state data is cryptographically guaranteed to come from its claimed source, and all communications flow through verified channels. This isn't over-engineering - it's the minimum required for nodes to trust each other in a truly distributed system without any central authority.
14
+ When you see the various key files and certificates being generated during setup, you're watching these three defensive layers being constructed. Each one is essential for maintaining the integrity and privacy of your distributed application.
@@ -0,0 +1,4 @@
1
+ from distributed_state_network.handler import DSNodeServer
2
+ from distributed_state_network.dsnode import DSNode
3
+ from distributed_state_network.objects.config import DSNodeConfig
4
+ from distributed_state_network.objects.endpoint import Endpoint
@@ -0,0 +1,10 @@
1
+ import sys
2
+ from util.aes import generate_aes_key
3
+
4
+ if len(sys.argv) < 2:
5
+ raise Exception("Usage: python create_key.py [output_key_file_location]")
6
+
7
+ location = sys.argv[1]
8
+
9
+ with open(location, 'wb') as f:
10
+ f.write(generate_aes_key())
@@ -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())