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.
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/.gitignore +2 -1
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/PKG-INFO +11 -42
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/README.md +10 -41
- distributed_state_network-0.0.3/documentation/ds-node-config.md +47 -0
- distributed_state_network-0.0.3/documentation/ds-node-server.md +56 -0
- distributed_state_network-0.0.3/documentation/ds-node.md +64 -0
- distributed_state_network-0.0.3/documentation/protocol.md +37 -0
- distributed_state_network-0.0.3/documentation/usage.md +68 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/pyproject.toml +1 -1
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/dsnode.py +9 -5
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/handler.py +7 -8
- distributed_state_network-0.0.3/tests/connections.py +91 -0
- distributed_state_network-0.0.1/security.md +0 -14
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/.github/workflows/publish.yml +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/LICENSE +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/coverage.sh +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/__init__.py +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/create_key.py +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/objects/config.py +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/objects/endpoint.py +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/objects/hello_packet.py +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/objects/peers_packet.py +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/objects/signed_packet.py +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/objects/state_packet.py +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/util/__init__.py +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/util/aes.py +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/util/byte_helper.py +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/util/ecdsa.py +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/util/https.py +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/src/distributed_state_network/util/key_manager.py +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/technical.md +0 -0
- {distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: distributed-state-network
|
|
3
|
-
Version: 0.0.
|
|
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
|
-
##
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
-
|
|
114
|
-
|
|
115
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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.
|
{distributed_state_network-0.0.1 → distributed_state_network-0.0.3}/.github/workflows/publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|