Skip to content

Commit df7ad2c

Browse files
committed
Added a logger and the ability to simulate faulty data. Demo also plots a graph of repuation over time
1 parent f8e8cd9 commit df7ad2c

8 files changed

Lines changed: 158 additions & 21 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ pyvenv.cfg
88
/.vscode
99
/src/__pycache__
1010
/.mypy_cache
11-
/__pycache__
11+
/__pycache__
12+
app.log

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ The project is currently at TRL 0. The PoISE consensus mechanism is in the early
3838
│ └── __init__.py # Empty file, for module creation
3939
│ └── consensus_mech.py # Code for the PoISE consensus mechanism
4040
│ └── dag.py # Code for the Directed Acyclic Graph ledger structure
41+
│ └── logger.py # Code for the app logger
4142
│ └── satellite_node.py # Code representing a satellite in the network
4243
│ └── transaction.py # Code representing a transaction submitted by a satellite
4344
│ └── utils.py # Utility functions and global variables

accord_demo.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@
3131
from skyfield.api import EarthSatellite
3232
from src.consensus_mech import ConsensusMechanism
3333
from src.dag import DAG
34+
from src.logger import get_logger
3435
from src.reputation import MAX_REPUTATION, ReputationManager
3536
from src.satellite_node import SatelliteNode
3637
from src.transaction import Transaction, TransactionMetadata
3738

39+
logger = get_logger(__name__)
3840

3941
def create_noisy_transaction(base_sat: EarthSatellite) -> Transaction:
4042
"""
@@ -70,7 +72,7 @@ def create_noisy_transaction(base_sat: EarthSatellite) -> Transaction:
7072

7173
async def run_consensus_demo(initial_n_tx: int = 3,
7274
n_test_tx: int = 1) -> tuple[Optional[DAG], Optional[list[float]]]:
73-
"""
75+
"""
7476
This function runs a demonstration of the ACCORD Distributed Ledger.
7577
The following steps are demonstrated:
7678
- Loading in TLE data from a JSON file
@@ -103,7 +105,7 @@ async def run_consensus_demo(initial_n_tx: int = 3,
103105
sat_rep_list: list = []
104106

105107
if test_satellite.tle_data[0] is None:
106-
print("No satellite data found.")
108+
logger.info("No satellite data found.")
107109
return None, None
108110

109111
base_sat = test_satellite.tle_data[0]
@@ -121,6 +123,25 @@ async def run_consensus_demo(initial_n_tx: int = 3,
121123
asyncio.create_task(test_dag.listen())
122124

123125
# TODO - get more data to simulate better, rather than adding the same data
126+
logger.info("Submitting Valid Transactions")
127+
for _ in range(n_test_tx):
128+
# Run consensus on a single satellite observation - the test transaction
129+
await test_satellite.submit_transaction(
130+
satellite=base_sat,
131+
recipient_address=123
132+
)
133+
sat_rep_list.append(test_satellite.reputation)
134+
135+
logger.info("Submitting Malicious Transaction")
136+
test_satellite.is_malicious = True
137+
await test_satellite.submit_transaction(
138+
satellite=test_satellite.tle_data[0],
139+
recipient_address=123
140+
)
141+
sat_rep_list.append(test_satellite.reputation)
142+
143+
logger.info("Submitting Valid Transactions")
144+
test_satellite.is_malicious = False
124145
for _ in range(n_test_tx):
125146
# Run consensus on a single satellite observation - the test transaction
126147
await test_satellite.submit_transaction(
@@ -130,8 +151,10 @@ async def run_consensus_demo(initial_n_tx: int = 3,
130151
sat_rep_list.append(test_satellite.reputation)
131152

132153
# Output results
133-
print(test_dag.ledger)
134-
print(sat_rep_list)
154+
# logger.info("Test DAG:")
155+
# logger.info(test_dag.ledger)
156+
# logger.info("Satellite Reputations")
157+
# logger.info(sat_rep_list)
135158
return test_dag, sat_rep_list
136159

137160

src/consensus_mech.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def get_correctness_score(self, sat: EarthSatellite, dag: DAG) -> float:
122122

123123
# Very basic check — mean motion deviation, inclination deviation
124124
past = matches[-1]
125-
motion_dev = abs(sat.model.no_kozai - float(past["MEAN_MOTION"]))
125+
motion_dev = abs(((sat.model.no_kozai / (2 * np.pi)) * 1440)- float(past["MEAN_MOTION"]))
126126
incl_dev = abs((sat.model.inclo * 180 / np.pi) - float(past["INCLINATION"]))
127127

128128
correctness_score = 1.0
@@ -143,6 +143,7 @@ def estimate_accuracy(self, sat: EarthSatellite) -> float:
143143
speed = np.linalg.norm(velocity)
144144

145145
deviation = abs(speed - ideal_speed)
146+
146147
if deviation < 0.3:
147148
return 1.0
148149
if deviation < 0.6:
@@ -157,19 +158,21 @@ def calculate_consensus_score(self, correctness: float,
157158
Weighted score. Tuneable weights.
158159
TODO - tune
159160
"""
160-
return 0.4 * correctness + 0.4 * accuracy + 0.2 * reputation
161+
return 0.7 * correctness + 0.3 * accuracy + 0.2 * reputation
161162

162163

163164
def proof_of_inter_satellite_evaluation(self, dag: DAG,
164165
sat_node: SatelliteNode,
165166
transaction: Transaction) -> bool:
166167
"""
167-
Returns a bool of it consensus has been reached
168-
NOTE: Assume one witnessed satellite per transaction
168+
Returns a bool of it consensus has been reached
169+
NOTE: Assume one witnessed satellite per transaction
169170
"""
170171
# 1) Turn transaction data into an EarthSatellite object
171172
# for validation using wsg4 and skyfield
172-
sat = build_earth_satellite_list_from_str(load.timescale(), transaction.tx_data)[0]
173+
sat = build_earth_satellite_list_from_str(load.timescale(),
174+
transaction.tx_data,
175+
sat_node.is_malicious)[0]
173176

174177
# 2) TODO Check if data is valid, if not - ignore.
175178
# If the list is empty, there is no data that can be valid
@@ -212,6 +215,8 @@ def proof_of_inter_satellite_evaluation(self, dag: DAG,
212215
accuracy_score,
213216
sat_node.reputation)
214217

218+
print(consensus_score >= self.consensus_threshold)
219+
215220
sat_node.reputation = sat_node.rep_manager.decay(sat_node.reputation)
216221

217222
# 7) if consensus reached - strong node (maybe affects node reputation?),

src/dag.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@
2727
import random
2828
from collections import OrderedDict
2929
from typing import TYPE_CHECKING
30+
from .logger import get_logger
3031
from .transaction import Transaction, TransactionMetadata
3132

3233
if TYPE_CHECKING:
3334
from .consensus_mech import ConsensusMechanism
3435

36+
logger = get_logger(__name__)
37+
3538
class DAG():
3639
"""
3740
A class representing the Directed Acyclic Graph (DAG) Distributed Ledger Technology.
@@ -57,7 +60,7 @@ async def listen(self) -> None:
5760
"""
5861
while True:
5962
transaction, satellite, future = await self.queue.get()
60-
print(f"DAG received transaction {transaction.hash}")
63+
logger.info("DAG received transaction %s", transaction.hash)
6164
consensus_result = self.consensus_mech.proof_of_inter_satellite_evaluation(
6265
dag=self,
6366
sat_node=satellite,

src/logger.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
The Autonomous Cooperative Consensus Orbit Determination (ACCORD) framework.
3+
Author: Beth Probert
4+
Email: beth.probert@strath.ac.uk
5+
6+
Copyright (C) 2025 Applied Space Technology Laboratory
7+
8+
This program is free software: you can redistribute it and/or modify
9+
it under the terms of the GNU General Public License as published by
10+
the Free Software Foundation, either version 3 of the License, or
11+
(at your option) any later version.
12+
13+
This program is distributed in the hope that it will be useful,
14+
but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
GNU General Public License for more details.
17+
18+
You should have received a copy of the GNU General Public License
19+
along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
"""
21+
22+
import logging
23+
from logging import Logger
24+
25+
def get_logger(name: str = __name__) -> Logger:
26+
"""
27+
Returns a logger for the application
28+
"""
29+
logger = logging.getLogger(name)
30+
if not logger.hasHandlers(): # Avoid adding multiple handlers
31+
logger.setLevel(logging.DEBUG)
32+
33+
# Console handler
34+
ch = logging.StreamHandler()
35+
ch.setLevel(logging.INFO) # Adjust level for console output
36+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
37+
ch.setFormatter(formatter)
38+
logger.addHandler(ch)
39+
40+
# File handler (optional)
41+
fh = logging.FileHandler('app.log')
42+
fh.setLevel(logging.DEBUG)
43+
fh.setFormatter(formatter)
44+
logger.addHandler(fh)
45+
46+
return logger

src/satellite_node.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# pylint: disable=too-many-instance-attributes
12
"""
23
The Autonomous Cooperative Consensus Orbit Determination (ACCORD) framework.
34
Author: Beth Probert
@@ -25,18 +26,22 @@
2526
from typing import Optional
2627
from skyfield.api import EarthSatellite
2728
from .dag import DAG
29+
from .logger import get_logger
2830
from .reputation import ReputationManager, MAX_REPUTATION
2931
from .transaction import Transaction, TransactionMetadata
3032
from .utils import build_tx_data_str, load_json_data
3133

34+
logger = get_logger(__name__)
35+
3236

3337
class SatelliteNode():
3438
"""
35-
A class representing a node in the network, in this case a LEO satellite.
39+
A class representing a node in the network, in this case a LEO satellite.
3640
This does NOT represent a node in the ledger - these are transactions
3741
"""
38-
def __init__(self, node_id: str, queue: asyncio.Queue) -> None:
42+
def __init__(self, node_id: str, queue: asyncio.Queue, is_malicious: bool = False) -> None:
3943
self.id: str = node_id
44+
self.is_malicious: bool = is_malicious
4045
self.queue = queue
4146
self.exp_pos: int = 0
4247
# Reputation starts at 0, affected by validity and accuracy
@@ -47,9 +52,11 @@ def __init__(self, node_id: str, queue: asyncio.Queue) -> None:
4752
self.rep_manager = ReputationManager()
4853
self.local_dag: Optional[DAG] = None
4954

50-
# This is for testing purposes. In reality, data will
55+
# TODO This is for testing purposes. In reality, data will
5156
# be loaded from a sensor
52-
self.tle_data: list[Optional[EarthSatellite]] = load_json_data("od_data.json")
57+
self.tle_data: list[Optional[EarthSatellite]] = load_json_data(
58+
"od_data.json",
59+
faulty_data = self.is_malicious)
5360

5461
async def submit_transaction(self,
5562
satellite: EarthSatellite,
@@ -77,7 +84,7 @@ async def submit_transaction(self,
7784
metadata=metadata)
7885

7986
future = asyncio.get_running_loop().create_future()
80-
print(f"Satellite {self.id}: submitting transaction {transaction.hash}")
87+
logger.info("Satellite %s: submitting transaction %s", self.id, transaction.hash)
8188
await self.queue.put((transaction, self, future))
8289
# Waits until DAG sets the result
8390
return await future

src/utils.py

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,46 @@
2525
from typing import Optional
2626
from skyfield.api import EarthSatellite, load, Timescale
2727
import numpy as np
28+
from sgp4.api import Satrec, WGS72 # type: ignore[import-untyped]
29+
30+
def corrupt_satellite(sat: EarthSatellite, mean_motion_factor: float = 0.8) -> EarthSatellite:
31+
"""
32+
mean motion factor of >1.05 or <0.73 makes the data invalid
33+
1.0 is the same.
34+
1.01 - 1.04 affects correctness
35+
0.74-0.93 affects accuracy and correctness
36+
0.94-0.99 affects correctness
37+
"""
38+
# Copy fields from the original model
39+
m = sat.model
40+
41+
# Build a new Satrec with modified mean motion
42+
corrupted_model = Satrec()
43+
corrupted_model.sgp4init(
44+
WGS72,
45+
'i',
46+
int(m.satnum),
47+
float(m.jdsatepoch - 2433281.5), # epoch in days since 1949-12-31
48+
float(m.bstar),
49+
float(m.ndot),
50+
float(m.nddot),
51+
float(m.ecco),
52+
float(m.argpo),
53+
float(m.inclo),
54+
float(m.mo),
55+
float(m.no_kozai) * mean_motion_factor, # corrupted mean motion
56+
float(m.nodeo)
57+
)
58+
59+
# Create an EarthSatellite instance without calling __init__
60+
corrupted_sat = EarthSatellite.__new__(EarthSatellite)
61+
corrupted_model.intldesg = sat.model.intldesg
62+
corrupted_sat.model = corrupted_model
63+
corrupted_sat.name = sat.name
64+
corrupted_sat.epoch = sat.epoch
65+
corrupted_sat.target = sat.target
66+
67+
return corrupted_sat
2868

2969

3070
def build_tx_data_str(satellite_data: EarthSatellite) -> str:
@@ -85,15 +125,18 @@ def build_tx_data_str(satellite_data: EarthSatellite) -> str:
85125
"MEAN_MOTION_DDOT": motion_ddot
86126
})
87127

88-
def build_earth_satellite_list_from_str(ts: Timescale, data: str) -> list[Optional[EarthSatellite]]:
128+
def build_earth_satellite_list_from_str(ts: Timescale, data: str,
129+
make_faulty: bool) -> list[Optional[EarthSatellite]]:
89130
"""
90131
Construct an EarthSatellite object from a string of data to be
91132
used for data validation.
92133
93134
Arguments:
94135
- ts: The timescale, used to build the satellite's epoch time
95-
- data: a string of data (ideally in a dict format) to be used to populate
136+
- data: A string of data (ideally in a dict format) to be used to populate
96137
the attributes of the EarthSatellite object.
138+
- make_faulty: A boolean flag about whether the data generated should be faulty, to
139+
represent a malicious node
97140
98141
Returns:
99142
- A list of EarthSatellite objects
@@ -112,13 +155,21 @@ def build_earth_satellite_list_from_str(ts: Timescale, data: str) -> list[Option
112155
if not isinstance(data, list) or not all(isinstance(d, dict) for d in data):
113156
raise TypeError("Expected a list of dicts after parsing satellite data")
114157

115-
return [EarthSatellite.from_omm(ts, fields) for fields in data]
158+
sat_list = [EarthSatellite.from_omm(ts, fields) for fields in data]
116159

117-
def load_json_data(file_name: str) -> list[Optional[EarthSatellite]]:
160+
if make_faulty:
161+
for i, sat in enumerate(sat_list):
162+
sat_list[i] = corrupt_satellite(sat)
163+
164+
return sat_list
165+
166+
def load_json_data(file_name: str, faulty_data: bool) -> list[Optional[EarthSatellite]]:
118167
"""
119168
Turns json data in a file into a Python dict.
120169
JSON data must be in the Celestrak format
121170
https://rhodesmill.org/skyfield/earth-satellites.html
171+
172+
If faulty_data is True, this function will generate faulty data for testing
122173
"""
123174
try:
124175
with load.open(file_name) as f:
@@ -128,5 +179,5 @@ def load_json_data(file_name: str) -> list[Optional[EarthSatellite]]:
128179
return []
129180

130181
ts = load.timescale()
131-
satellite_list = build_earth_satellite_list_from_str(ts, data)
182+
satellite_list = build_earth_satellite_list_from_str(ts, data, faulty_data)
132183
return satellite_list

0 commit comments

Comments
 (0)