distributed-state-network 0.0.1__tar.gz → 0.0.3__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 (32) hide show
  1. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/.gitignore +2 -1
  2. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/PKG-INFO +11 -42
  3. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/README.md +10 -41
  4. distributed_state_network-0.0.3/documentation/ds-node-config.md +47 -0
  5. distributed_state_network-0.0.3/documentation/ds-node-server.md +56 -0
  6. distributed_state_network-0.0.3/documentation/ds-node.md +64 -0
  7. distributed_state_network-0.0.3/documentation/protocol.md +37 -0
  8. distributed_state_network-0.0.3/documentation/usage.md +68 -0
  9. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/pyproject.toml +1 -1
  10. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/dsnode.py +9 -5
  11. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/handler.py +7 -8
  12. distributed_state_network-0.0.3/tests/connections.py +91 -0
  13. distributed_state_network-0.0.1/security.md +0 -14
  14. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/.github/workflows/publish.yml +0 -0
  15. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/LICENSE +0 -0
  16. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/coverage.sh +0 -0
  17. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/__init__.py +0 -0
  18. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/create_key.py +0 -0
  19. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/objects/config.py +0 -0
  20. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/objects/endpoint.py +0 -0
  21. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/objects/hello_packet.py +0 -0
  22. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/objects/peers_packet.py +0 -0
  23. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/objects/signed_packet.py +0 -0
  24. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/objects/state_packet.py +0 -0
  25. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/util/__init__.py +0 -0
  26. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/util/aes.py +0 -0
  27. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/util/byte_helper.py +0 -0
  28. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/util/ecdsa.py +0 -0
  29. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/util/https.py +0 -0
  30. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/util/key_manager.py +0 -0
  31. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/technical.md +0 -0
  32. {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/test.py +0 -0
@@ -7,4 +7,5 @@ test.key
7
7
  .coverage
8
8
  count.py
9
9
  appended.py
10
- py_line_count_history.jsonl
10
+ py_line_count_history.jsonl
11
+ network.key
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: distributed-state-network
3
- Version: 0.0.1
3
+ Version: 0.0.3
4
4
  Summary: A tool to distribute the state of a network device to other devices on the network
5
5
  Project-URL: Homepage, https://github.com/erinclemmer/distributed_state_network
6
6
  Project-URL: Issues, https://github.com/erinclemmer/distributed_state_network/issues
@@ -79,29 +79,7 @@ DSN creates a peer-to-peer network where each node maintains its own state datab
79
79
  - Any node can read any other node's state instantly
80
80
  - All communication is encrypted with AES + ECDSA + HTTPS
81
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
82
+ ## Example: Distributed Temperature Monitoring
105
83
 
106
84
  Create a network of temperature sensors that share readings:
107
85
 
@@ -129,21 +107,12 @@ for node_id in monitor.node.peers():
129
107
  temp = monitor.node.read_data(node_id, "temperature")
130
108
  print(f"{node_id}: {temp}°F")
131
109
  ```
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)
110
+
111
+ ### Documentation
112
+ * [Usage Examples](./documentation/usage.md)
113
+ * [Protocol](./documentation/protocol.md)
114
+
115
+ #### API Reference
116
+ * [DSNodeServer](./documentation/ds-node-server.md)
117
+ * [DSNode](./documentation/ds-node.md)
118
+ * [DSNodeConfig](./documentation/ds-node-config.md)
@@ -54,29 +54,7 @@ DSN creates a peer-to-peer network where each node maintains its own state datab
54
54
  - Any node can read any other node's state instantly
55
55
  - All communication is encrypted with AES + ECDSA + HTTPS
56
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
57
+ ## Example: Distributed Temperature Monitoring
80
58
 
81
59
  Create a network of temperature sensors that share readings:
82
60
 
@@ -104,21 +82,12 @@ for node_id in monitor.node.peers():
104
82
  temp = monitor.node.read_data(node_id, "temperature")
105
83
  print(f"{node_id}: {temp}°F")
106
84
  ```
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)
85
+
86
+ ### Documentation
87
+ * [Usage Examples](./documentation/usage.md)
88
+ * [Protocol](./documentation/protocol.md)
89
+
90
+ #### API Reference
91
+ * [DSNodeServer](./documentation/ds-node-server.md)
92
+ * [DSNode](./documentation/ds-node.md)
93
+ * [DSNodeConfig](./documentation/ds-node-config.md)
@@ -0,0 +1,47 @@
1
+ ## DSNodeConfig
2
+
3
+ Configuration object for initializing a DSNode instance.
4
+
5
+ ```python
6
+ from distributed_state_network import DSNodeConfig
7
+ ```
8
+
9
+ ### Class Definition
10
+ ```python
11
+ @dataclass(frozen=True)
12
+ class DSNodeConfig:
13
+ node_id: str
14
+ port: int
15
+ aes_key_file: str
16
+ bootstrap_nodes: List[Endpoint]
17
+ ```
18
+
19
+ ### Attributes
20
+ - **node_id** (`str`): Unique identifier for the node
21
+ - **port** (`int`): Port number for the node to listen on
22
+ - **aes_key_file** (`str`): Path to the AES key file for encryption/decryption
23
+ - **bootstrap_nodes** (`List[Endpoint]`): List of initial nodes to connect to when joining the network
24
+
25
+ ### Methods
26
+
27
+ ### `from_dict(data: Dict) -> DSNodeConfig`
28
+ Creates a DSNodeConfig instance from a dictionary.
29
+
30
+ **Parameters:**
31
+ - `data` (`Dict`): Dictionary containing configuration parameters
32
+
33
+ **Returns:**
34
+ - `DSNodeConfig`: Configuration instance
35
+
36
+ **Example:**
37
+ ```python
38
+ config_dict = {
39
+ "node_id": "node1",
40
+ "port": 8000,
41
+ "aes_key_file": "/path/to/key.aes",
42
+ "bootstrap_nodes": [
43
+ {"address": "127.0.0.1", "port": 8001}
44
+ ]
45
+ }
46
+ config = DSNodeConfig.from_dict(config_dict)
47
+ ```
@@ -0,0 +1,56 @@
1
+ ## DSNodeServer
2
+
3
+ HTTPS server wrapper for DSNode that handles incoming network requests.
4
+
5
+ ```python
6
+ from distributed_state_network import DSNodeServer
7
+ ```
8
+
9
+ ### Class Definition
10
+ ```python
11
+ class DSNodeServer(HTTPServer):
12
+ config: DSNodeConfig
13
+ node: DSNode
14
+ ```
15
+
16
+ ### Constructor
17
+
18
+ **Parameters:**
19
+ - `config` (`DSNodeConfig`): Node configuration
20
+ - `disconnect_callback` (`Optional[Callable]`): Callback for disconnect events
21
+
22
+ ### Static Methods
23
+
24
+ ### `start(config: DSNodeConfig, disconnect_callback: Optional[Callable] = None) -> DSNodeServer`
25
+ Creates and starts a new DSNodeServer instance with SSL configuration.
26
+ ```python
27
+ server = DSNodeServer.start(config)
28
+ ```
29
+
30
+ **Parameters:**
31
+ - `config` (`DSNodeConfig`): Node configuration
32
+ - `disconnect_callback` (`Optional[Callable]`): Callback for disconnect events
33
+
34
+ **Returns:**
35
+ - `DSNodeServer`: Running server instance
36
+
37
+ ### `generate_key(out_file_path: str) -> None`
38
+ Generates a new AES key file for network encryption.
39
+
40
+ **Parameters:**
41
+ - `out_file_path` (`str`): Path where the key file will be saved
42
+
43
+ **Example:**
44
+ ```python
45
+ DSNodeServer.generate_key("/path/to/network.key")
46
+ ```
47
+
48
+ ### Instance Methods
49
+
50
+ ### `stop() -> None`
51
+ Gracefully shuts down the server and cleans up resources.
52
+
53
+ **Example:**
54
+ ```python
55
+ server.stop()
56
+ ```
@@ -0,0 +1,64 @@
1
+ ## DSNode
2
+
3
+ Main node class that handles all distributed state network operations including peer management, state synchronization, and encrypted communication.
4
+
5
+ ```python
6
+ from distributed_state_network import DSNode
7
+ ```
8
+
9
+ ### Class Definition
10
+ ```python
11
+ class DSNode:
12
+ config: DSNodeConfig
13
+ address_book: Dict[str, Endpoint]
14
+ node_states: Dict[str, StatePacket]
15
+ shutting_down: bool = False
16
+ ```
17
+
18
+ ### Constructor
19
+
20
+ ```python
21
+ node = DSNode(config, "0.0.3", disconnect_callback=on_disconnect)
22
+ ```
23
+
24
+ **Parameters:**
25
+ - `config` (`DSNodeConfig`): Node configuration
26
+ - `version` (`str`): Protocol version string for compatibility checking
27
+ - `disconnect_callback` (`Optional[Callable]`): Callback function invoked when a peer disconnects
28
+
29
+ ### Key Methods
30
+
31
+ ### `bootstrap(con: Endpoint) -> None`
32
+ Connects to a bootstrap node to join the network.
33
+
34
+ **Parameters:**
35
+ - `con` (`Endpoint`): Connection endpoint of the bootstrap node
36
+
37
+ **Raises:**
38
+ - `Exception`: If connection to bootstrap node fails
39
+
40
+ ### `update_data(key: str, val: str) -> None`
41
+ Updates a key-value pair in the node's state and broadcasts the update to all peers.
42
+ ```python
43
+ node.update_data("status", "active")
44
+ ```
45
+
46
+ **Parameters:**
47
+ - `key` (`str`): State key to update
48
+ - `val` (`str`): New value for the key
49
+
50
+ ### `read_data(node_id: str, key: str) -> Optional[str]`
51
+ Reads a value from a specific node's state.
52
+
53
+ **Parameters:**
54
+ - `node_id` (`str`): ID of the node to read from
55
+ - `key` (`str`): Key to retrieve
56
+
57
+ **Returns:**
58
+ - `Optional[str]`: Value if exists, None otherwise
59
+
60
+ ### `peers() -> List[str]`
61
+ Returns a list of all connected peer node IDs.
62
+
63
+ **Returns:**
64
+ - `List[str]`: List of node IDs
@@ -0,0 +1,37 @@
1
+ ## Network Protocol
2
+
3
+ ### Endpoints
4
+ The server exposes the following HTTPS endpoints:
5
+
6
+ - **POST /hello**: Exchange node information and certificates
7
+ - **POST /peers**: Request/share peer list
8
+ - **POST /update**: Send/receive state updates
9
+ - **POST /ping**: Connectivity check
10
+
11
+ ### Security
12
+ - All communication is encrypted using AES with a shared key
13
+ - Messages are signed using ECDSA for authentication
14
+ - HTTPS with self-signed certificates for transport security
15
+
16
+ ### State Synchronization
17
+ - Nodes maintain a copy of all peers' states
18
+ - Updates are broadcast to all connected peers
19
+ - Timestamps prevent older updates from overwriting newer ones
20
+
21
+ ## Important Notes
22
+
23
+ 1. **Shared AES Key**: All nodes in the network must use the same AES key file
24
+ 2. **Unique Node IDs**: Each node must have a unique node_id
25
+ 3. **Port Availability**: Ensure the specified port is available before starting
26
+ 4. **Bootstrap Nodes**: At least one bootstrap node is required to join an existing network
27
+ 5. **Network Tick**: The network performs maintenance checks every 3 seconds
28
+ 6. **Certificate Management**: Certificates and keys are automatically generated and stored in `certs/` and `credentials/` directories
29
+
30
+ ## Error Handling
31
+
32
+ Common HTTP status codes returned:
33
+ - **200**: Success
34
+ - **401**: Not Authorized (signature verification failed)
35
+ - **404**: Endpoint not found
36
+ - **406**: Not Acceptable (invalid data or version mismatch)
37
+ - **505**: Version not supported
@@ -0,0 +1,68 @@
1
+ ## Usage Examples
2
+
3
+ ### Basic Setup
4
+
5
+ ```python
6
+ from distributed_state_network import DSNodeServer, DSNodeConfig, Endpoint
7
+
8
+ # Create configuration
9
+ config = DSNodeConfig(
10
+ node_id="node1",
11
+ port=8000,
12
+ aes_key_file="/path/to/shared.key",
13
+ bootstrap_nodes=[] # Empty for first node
14
+ )
15
+
16
+ # Start server
17
+ server = DSNodeServer.start(config)
18
+
19
+ # Update state
20
+ server.node.update_data("status", "online")
21
+ server.node.update_data("version", "1.0.0")
22
+
23
+ # Read own state
24
+ my_status = server.node.read_data("node1", "status")
25
+
26
+ # Get connected peers
27
+ peers = server.node.peers()
28
+ print(f"Connected peers: {peers}")
29
+
30
+ # Shutdown
31
+ server.stop()
32
+ ```
33
+
34
+ ### Joining Existing Network
35
+
36
+ ```python
37
+ # Node 2 configuration with bootstrap
38
+ config2 = DSNodeConfig(
39
+ node_id="node2",
40
+ port=8001,
41
+ aes_key_file="/path/to/shared.key", # Same key file as network
42
+ bootstrap_nodes=[
43
+ Endpoint("127.0.0.1", 8000) # Node 1's endpoint
44
+ ]
45
+ )
46
+
47
+ server2 = DSNodeServer.start(config2)
48
+
49
+ # Read state from peer
50
+ peer_status = server2.node.read_data("node1", "status")
51
+ ```
52
+
53
+ ### With Disconnect Callback
54
+
55
+ ```python
56
+ def handle_disconnect():
57
+ print("A peer has disconnected!")
58
+ # Handle reconnection logic
59
+
60
+ config = DSNodeConfig(
61
+ node_id="node3",
62
+ port=8002,
63
+ aes_key_file="/path/to/shared.key",
64
+ bootstrap_nodes=[Endpoint("127.0.0.1", 8000)]
65
+ )
66
+
67
+ server = DSNodeServer.start(config, disconnect_callback=handle_disconnect)
68
+ ```
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "distributed-state-network"
7
- version = "0.0.1"
7
+ version = "0.0.3"
8
8
  authors = [{name="Erin Clemmer", email="erin.c.clemmer@gmail.com"}]
9
9
  description = "A tool to distribute the state of a network device to other devices on the network"
10
10
  keywords = ["distributed", "networking"]
@@ -1,12 +1,11 @@
1
1
  import os
2
2
  import time
3
- import json
4
3
  import logging
5
4
 
6
5
  import requests
7
6
  import threading
8
7
  from requests import RequestException
9
- from typing import Dict, Tuple, List, Optional
8
+ from typing import Dict, Tuple, List, Optional, Callable
10
9
 
11
10
  from distributed_state_network.objects.endpoint import Endpoint
12
11
  from distributed_state_network.objects.hello_packet import HelloPacket
@@ -16,7 +15,7 @@ from distributed_state_network.objects.config import DSNodeConfig
16
15
 
17
16
  from distributed_state_network.util import get_dict_hash
18
17
  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
18
+ from distributed_state_network.util.aes import aes_encrypt, aes_decrypt
20
19
 
21
20
  TICK_INTERVAL = 3
22
21
 
@@ -30,6 +29,7 @@ class DSNode:
30
29
  self,
31
30
  config: DSNodeConfig,
32
31
  version: str,
32
+ disconnect_callback: Optional[Callable] = None
33
33
  ):
34
34
  self.config = config
35
35
  self.version = version
@@ -49,6 +49,7 @@ class DSNode:
49
49
  }
50
50
 
51
51
  self.logger = logging.getLogger("DSN: " + config.node_id)
52
+ self.disconnect_cb = disconnect_callback
52
53
  if not os.path.exists(config.aes_key_file):
53
54
  raise Exception(f"Could not find aes key file in {config.aes_key_file}")
54
55
  threading.Thread(target=self.network_tick).start()
@@ -60,7 +61,7 @@ class DSNode:
60
61
  def network_tick(self):
61
62
  time.sleep(TICK_INTERVAL)
62
63
  if self.shutting_down:
63
- self.logger.info(f"Shutting down node")
64
+ self.logger.info("Shutting down node")
64
65
  return
65
66
  self.test_connections()
66
67
  threading.Thread(target=self.network_tick).start()
@@ -69,9 +70,10 @@ class DSNode:
69
70
  def remove(node_id: str):
70
71
  if node_id in self.node_states:
71
72
  del self.node_states[node_id]
73
+ del self.address_book[node_id]
72
74
  self.logger.info(f"PING failed for {node_id}, disconnecting...")
73
75
  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:
76
+ if node_id not in self.node_states or node_id == self.config.node_id:
75
77
  continue
76
78
  try:
77
79
  if self.shutting_down:
@@ -80,6 +82,8 @@ class DSNode:
80
82
  except RequestException:
81
83
  if node_id in self.node_states: # double check if something has changed since the ping request started
82
84
  remove(node_id)
85
+ if self.disconnect_cb is not None:
86
+ self.disconnect_cb()
83
87
 
84
88
  def send_request_to_node(self, node_id: str, path: str, payload: bytes, verify) -> Tuple[requests.Response, bytes]:
85
89
  con = self.connection_from_node(node_id)
@@ -1,9 +1,7 @@
1
1
  import ssl
2
2
  import threading
3
- import json
4
3
  import logging
5
- from typing import Tuple, Callable, Dict
6
- from threading import Thread
4
+ from typing import Callable, Optional
7
5
  from http.server import HTTPServer, BaseHTTPRequestHandler
8
6
 
9
7
  from distributed_state_network.dsnode import DSNode
@@ -11,7 +9,7 @@ from distributed_state_network.objects.config import DSNodeConfig
11
9
  from distributed_state_network.util.aes import generate_aes_key
12
10
  from distributed_state_network.util import stop_thread
13
11
 
14
- VERSION = "0.0.1"
12
+ VERSION = "0.0.3"
15
13
  logging.basicConfig(level=logging.INFO)
16
14
 
17
15
  def respond_bytes(handler: BaseHTTPRequestHandler, data: bytes):
@@ -71,11 +69,12 @@ def serve(httpd):
71
69
  class DSNodeServer(HTTPServer):
72
70
  def __init__(
73
71
  self,
74
- config: DSNodeConfig
72
+ config: DSNodeConfig,
73
+ disconnect_callback: Optional[Callable] = None
75
74
  ):
76
75
  super().__init__(("127.0.0.1", config.port), DSNodeHandler)
77
76
  self.config = config
78
- self.node = DSNode(config, VERSION)
77
+ self.node = DSNode(config, VERSION, disconnect_callback)
79
78
  self.node.logger.info(f'Started DSNode on port {config.port}')
80
79
 
81
80
  def stop(self):
@@ -91,8 +90,8 @@ class DSNodeServer(HTTPServer):
91
90
  f.write(key)
92
91
 
93
92
  @staticmethod
94
- def start(config: DSNodeConfig) -> 'NodeServer':
95
- n = DSNodeServer(config)
93
+ def start(config: DSNodeConfig, disconnect_callback: Optional[Callable] = None) -> 'NodeServer':
94
+ n = DSNodeServer(config, disconnect_callback)
96
95
  ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
97
96
  public_path = n.node.cert_manager.public_path(n.config.node_id)
98
97
  ssl_context.load_cert_chain(
@@ -0,0 +1,91 @@
1
+ import os
2
+ import sys
3
+ import time
4
+ import unittest
5
+ from typing import Optional, Callable
6
+
7
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../src'))
8
+
9
+ from distributed_state_network import DSNodeServer, DSNodeConfig, Endpoint
10
+
11
+ KEY_FILE = "network.key"
12
+
13
+ if not os.path.exists(KEY_FILE):
14
+ DSNodeServer.generate_key("network.key")
15
+
16
+ d_cb_test = 0
17
+
18
+ current_port = 5000
19
+
20
+ def start_node(node_id: str, bootstrap_port: int = None, disconnect_cb: Optional[Callable] = None):
21
+ global current_port
22
+ args = {
23
+ "node_id": node_id,
24
+ "port": current_port,
25
+ "aes_key_file": KEY_FILE
26
+ }
27
+ current_port += 1
28
+ if bootstrap_port is not None:
29
+ args["bootstrap_nodes"] = [Endpoint(
30
+ address="127.0.0.1",
31
+ port=bootstrap_port
32
+ )]
33
+ else:
34
+ args["bootstrap_nodes"] = []
35
+
36
+ return DSNodeServer.start(DSNodeConfig(**args), disconnect_cb)
37
+
38
+ class ConnectionsTest(unittest.TestCase):
39
+ def test_one(self):
40
+ node = start_node("node-1")
41
+ self.assertEqual(["node-1"], node.node.peers())
42
+
43
+ def test_two(self):
44
+ node1 = start_node("node-1")
45
+ node2 = start_node("node-2", node1.config.port)
46
+ self.assertEqual(["node-1", "node-2"], sorted(node1.node.peers()))
47
+ self.assertEqual(["node-1", "node-2"], sorted(node2.node.peers()))
48
+
49
+ def test_many(self):
50
+ bootstrap_node = start_node("bootstrap")
51
+ nodes = []
52
+ for i in range(10):
53
+ nodes.append(start_node(f"node-{i}", bootstrap_node.config.port))
54
+
55
+ node_list = ["bootstrap"]
56
+ node_list.extend([f"node-{i}" for i in range(10)])
57
+
58
+ time.sleep(5)
59
+ self.assertEqual(node_list, sorted(bootstrap_node.node.peers()))
60
+ for i in range(10):
61
+ self.assertEqual(node_list, sorted(nodes[i].node.peers()))
62
+
63
+ def test_disconnect(self):
64
+ node1 = start_node("node-1")
65
+ node2 = start_node("node-2", node1.config.port)
66
+ node3 = start_node("node-3", node1.config.port)
67
+
68
+ time.sleep(1)
69
+ node2.stop()
70
+ time.sleep(10)
71
+ node4 = start_node("node-4", node1.config.port)
72
+ time.sleep(10)
73
+ self.assertEqual(["node-1", "node-3", "node-4"], sorted(node1.node.peers()))
74
+ self.assertEqual(["node-1", "node-3", "node-4"], sorted(node3.node.peers()))
75
+ self.assertEqual(["node-1", "node-3", "node-4"], sorted(node4.node.peers()))
76
+
77
+ def test_disconnect_cb(self):
78
+ global d_cb_test
79
+ d_cb_test = 0
80
+ def inc():
81
+ global d_cb_test
82
+ d_cb_test = 1
83
+ node1 = start_node("node-1", None, inc)
84
+ node2 = start_node("node-2", node1.config.port)
85
+ time.sleep(1)
86
+ node2.stop()
87
+ time.sleep(5)
88
+ self.assertEqual(d_cb_test, 1)
89
+
90
+ if __name__ == '__main__':
91
+ unittest.main()
@@ -1,14 +0,0 @@
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.