implement scenario 2

This commit is contained in:
Florian Förster 2025-12-18 11:36:22 +01:00
parent 56bbc22a41
commit 1235b5bb4f

View File

@ -2,11 +2,13 @@
import dataclasses as dc import dataclasses as dc
import hashlib import hashlib
import json import json
import random
import sys import sys
from pathlib import Path from pathlib import Path
from typing import cast from typing import cast
import click import click
import dopt_basics.datetime as dopt_dt
import polars as pl import polars as pl
import rich.box import rich.box
import rich.table import rich.table
@ -17,6 +19,8 @@ from rich.console import Console
from dopt_pollublock_blockchain import blockchain, db, types from dopt_pollublock_blockchain import blockchain, db, types
from dopt_pollublock_blockchain import constants as const from dopt_pollublock_blockchain import constants as const
user_seed: int | None = const.RNG_DEFAULT_SEED
console = Console() console = Console()
p_base = io.search_folder_path(Path(__file__), "src", return_inclusive=False) p_base = io.search_folder_path(Path(__file__), "src", return_inclusive=False)
assert p_base is not None assert p_base is not None
@ -27,8 +31,7 @@ assert p_sensor_data.exists(), "sensor data path not found"
engine = sql.create_engine(f"sqlite:///{str(p_sensor_data)}") engine = sql.create_engine(f"sqlite:///{str(p_sensor_data)}")
# %% # // common helper functions
# // common
def print_exception( def print_exception(
console: Console, console: Console,
error_txt: str, error_txt: str,
@ -112,6 +115,24 @@ def df_transform_hashing(
return q.collect() return q.collect()
def sample_entry(
rng: random.Random,
) -> types.RandomSampleEntry:
temp = round(rng.gauss(const.TEMPERATURE_MEAN, const.TEMPERATURE_STD), 2)
pressure = rng.choice(range(const.PRESSURE_MIN, const.PRESSURE_MAX + 1))
air_qty = round(rng.gauss(const.AIRQTY_MEAN, const.AIRQTY_STD), 6)
sampled_entry: types.RandomSampleEntry = {
"Datetime": dopt_dt.current_time_tz(cut_microseconds=True),
"Temperature_(Celsius)": temp,
"Pressure_(Pa)": pressure,
"Air_Quantity_(Percent)": air_qty,
"Blockchain_Block_Number": None,
}
return sampled_entry
# // blockchain # // blockchain
def blockchain_init() -> blockchain.Blockchain: def blockchain_init() -> blockchain.Blockchain:
console.print("Einlesen der Blockchain...") console.print("Einlesen der Blockchain...")
@ -126,6 +147,10 @@ def blockchain_init() -> blockchain.Blockchain:
console.print_exception(max_frames=20) console.print_exception(max_frames=20)
sys.exit(1) sys.exit(1)
console.print(
f"Die Blockchain enthält [bold]{len(chain)} Blöcke [/bold]. Darin eingeschlossen "
f"ist der initiale Start-Block, auch 'Genesis-Block' genannt."
)
console.print("Validiere Blockchain...") console.print("Validiere Blockchain...")
console.print("Prüfe Hashwerte und korrekte Verkettung...") console.print("Prüfe Hashwerte und korrekte Verkettung...")
try: try:
@ -143,8 +168,9 @@ def blockchain_init() -> blockchain.Blockchain:
return chain return chain
# // begin of both scenarios def start_seeding() -> bool:
def start_seeding() -> None: global user_seed
console.print( console.print(
"\nWir arbeiten mit zufälligen Werten. Damit die Ergebnisse trotzdem reproduzierbar " "\nWir arbeiten mit zufälligen Werten. Damit die Ergebnisse trotzdem reproduzierbar "
"sind, nutzen wir einen Initialwert für den Zufallsgenerator, den Sie " "sind, nutzen wir einen Initialwert für den Zufallsgenerator, den Sie "
@ -155,7 +181,10 @@ def start_seeding() -> None:
try: try:
user_input = console.input("[italic]Wählen Sie einen Wert zwischen 1 und 100: ") user_input = console.input("[italic]Wählen Sie einen Wert zwischen 1 und 100: ")
if user_input.strip(" ") not in const.RNG_SEED_RANGE: if user_input.strip(" ") not in const.RNG_SEED_RANGE:
console.print("Es sind nur Werte zwischen '1' und '100' als Eingabe zulässig") console.print(
"Es sind nur Werte zwischen [bold]'1'[/bold] und [bold]'100'"
"[/bold] als Eingabe zulässig"
)
continue continue
rng_seed = int(user_input) rng_seed = int(user_input)
except Exception: except Exception:
@ -163,15 +192,31 @@ def start_seeding() -> None:
sys.exit(1) sys.exit(1)
console.print(f"Sie haben '{rng_seed}' gewählt") console.print(f"Sie haben '{rng_seed}' gewählt")
const.RNG.seed(rng_seed) user_seed = rng_seed
const.RNG.seed(user_seed)
console.print( console.print(
f"[green]:heavy_check_mark:[/green] Der Zufallsgenerator wurde erfolgreich mit " f"[green]:heavy_check_mark:[/green] Der Zufallsgenerator wurde erfolgreich mit "
f"Wert '{rng_seed}' initialisiert" f"Wert '{user_seed}' initialisiert"
) )
click.confirm("Fortfahren?", default=True)
if not click.confirm("Fortfahren?", default=True):
return True
return False
def scenario_1(chain: blockchain.Blockchain) -> None: def reseed() -> bool:
console.print("Zunächst initialisieren wir den Zufallsgenerator erneut...")
const.RNG.seed(user_seed)
console.print(
f"[green]:heavy_check_mark:[/green] Der Zufallsgenerator wurde erfolgreich mit "
f"Wert '{user_seed}' initialisiert"
)
if not click.confirm("Fortfahren?", default=True):
return True
return False
def scenario_1(chain: blockchain.Blockchain) -> bool:
console.rule("[bold]Szenario 1 - Datenvalidierung", style="yellow3") console.rule("[bold]Szenario 1 - Datenvalidierung", style="yellow3")
max_idx: int = 0 max_idx: int = 0
@ -184,7 +229,8 @@ def scenario_1(chain: blockchain.Blockchain) -> None:
sys.exit(1) sys.exit(1)
console.print(f"Die Datenbank mit den Sensorwerten hat insgesamt {max_idx} Einträge") console.print(f"Die Datenbank mit den Sensorwerten hat insgesamt {max_idx} Einträge")
click.confirm("Fortfahren?", default=True) if not click.confirm("Fortfahren?", default=True):
return True
chosen_idx = const.RNG.choice(range(1, max_idx)) chosen_idx = const.RNG.choice(range(1, max_idx))
try: try:
@ -203,7 +249,8 @@ def scenario_1(chain: blockchain.Blockchain) -> None:
f"Zufällig ausgewählt wurde der Eintrag Nr. {chosen_idx} mit folgenden Eigenschaften:" f"Zufällig ausgewählt wurde der Eintrag Nr. {chosen_idx} mit folgenden Eigenschaften:"
) )
console.print(table) console.print(table)
click.confirm("Fortfahren?", default=True) if not click.confirm("Fortfahren?", default=True):
return True
try: try:
df_hash = df_transform_hashing(db_entry) df_hash = df_transform_hashing(db_entry)
@ -224,7 +271,8 @@ def scenario_1(chain: blockchain.Blockchain) -> None:
f"Wir nutzen SHA256 als Hash-Algorithmus. Dies gibt uns den folgenden Hashwert zurück: " f"Wir nutzen SHA256 als Hash-Algorithmus. Dies gibt uns den folgenden Hashwert zurück: "
f"[bold]{hashed_data_hex}[/bold]", f"[bold]{hashed_data_hex}[/bold]",
) )
click.confirm("Fortfahren?", default=True) if not click.confirm("Fortfahren?", default=True):
return True
try: try:
block_number = df_hash.select("Blockchain_Block_Number").item() block_number = df_hash.select("Blockchain_Block_Number").item()
@ -244,20 +292,23 @@ def scenario_1(chain: blockchain.Blockchain) -> None:
f"Block-Nummer erhalten wir aus der Datenbank (siehe Tabelle oben). Sie " f"Block-Nummer erhalten wir aus der Datenbank (siehe Tabelle oben). Sie "
f"lautet: {block_number}" f"lautet: {block_number}"
) )
click.confirm("Fortfahren?", default=True) if not click.confirm("Fortfahren?", default=True):
return True
console.print( console.print(
"Wir fragen den Block mit dieser Nummer aus der Blockchain ab und " "Wir fragen den Block mit dieser Nummer aus der Blockchain ab und "
"erhalten dazu folgende Daten:" "erhalten dazu folgende Daten:"
) )
console.print_json(py_block_json) console.print_json(py_block_json)
click.confirm("Fortfahren?", default=True) if not click.confirm("Fortfahren?", default=True):
return True
console.print( console.print(
f"Der Hashwert der dazugehörigen Daten lautet also demnach: " f"Der Hashwert der dazugehörigen Daten lautet also demnach: "
f"[bold]{py_block_data_hash}[/bold]" f"[bold]{py_block_data_hash}[/bold]"
) )
click.confirm("Fortfahren?", default=True) if not click.confirm("Fortfahren?", default=True):
return True
console.print("Zum Vergleich hier nochmals beide Hashwerte zusammen:") console.print("Zum Vergleich hier nochmals beide Hashwerte zusammen:")
console.print(f"Ermittelter Hashwert der Einträge:\t[bold]{hashed_data_hex}[/bold]") console.print(f"Ermittelter Hashwert der Einträge:\t[bold]{hashed_data_hex}[/bold]")
@ -266,11 +317,186 @@ def scenario_1(chain: blockchain.Blockchain) -> None:
"[green3]Wir sehen, dass beide Werte übereinstimmen. Das bedeutet, dass der Datensatz " "[green3]Wir sehen, dass beide Werte übereinstimmen. Das bedeutet, dass der Datensatz "
"integer ist und seit der Referenzierung in der Blockchain nicht verändert wurde." "integer ist und seit der Referenzierung in der Blockchain nicht verändert wurde."
) )
return False
def scenario_2(chain: blockchain.Blockchain) -> bool:
console.rule("[bold]Szenario 2 - Datengenerierung", style="yellow3")
max_idx: int = 0
try:
with engine.connect() as con:
res = con.execute(sql.select(sql.func.max(db.sensor_data.c.Index)))
max_idx = cast(int, res.scalar())
except Exception:
console.print_exception(max_frames=20)
sys.exit(1)
console.print(f"Die Datenbank mit den Sensorwerten hat insgesamt {max_idx} Einträge")
if not click.confirm("Fortfahren?", default=True):
return True
console.print(
"\nNun werden wir einen Datenbankeintrag zufällig generieren und abspeichern"
)
try:
sampled_entry = sample_entry(const.RNG)
stmt_insert = sql.insert(db.sensor_data).values(sampled_entry)
stmt_sel = sql.select(db.sensor_data).order_by(db.sensor_data.c.Index.desc()).limit(1)
with engine.begin() as con:
res = con.execute(stmt_insert, sampled_entry)
if res.rowcount <= 0:
raise RuntimeError("Database query not successful")
db_entry = pl.read_database(
stmt_sel, engine, schema_overrides=db.sensor_data_query_schema
)
table = rich_table_from_db_entry(db_entry, title="Zufällig generierte Sensordaten")
except Exception:
console.print_exception(max_frames=20)
sys.exit(1)
max_idx: int = 0
try:
with engine.connect() as con:
res = con.execute(sql.select(sql.func.max(db.sensor_data.c.Index)))
max_idx = cast(int, res.scalar())
except Exception:
console.print_exception(max_frames=20)
sys.exit(1)
console.print(f"Die Datenbank mit den Sensorwerten hat nun insgesamt {max_idx} Einträge")
console.print(f"Eingefügt wurde der Eintrag Nr. {max_idx} mit folgenden Eigenschaften:")
console.print(table)
if not click.confirm("Fortfahren?", default=True):
return True
try:
df_hash = df_transform_hashing(db_entry)
data_to_hash = df_hash.select("combined").item()
sha256 = hashlib.sha256()
sha256.update(data_to_hash.encode("UTF-8"))
hashed_data_hex = sha256.hexdigest()
except Exception:
console.print_exception(max_frames=20)
sys.exit(1)
console.print(
f"Die relevanten Eigenschaften kombinieren wir zu einem gemeinsamen "
f"Informationsblock: [bold]{data_to_hash}[/bold]",
highlight=False,
)
console.print(
f"Wir nutzen SHA256 als Hash-Algorithmus. Dies gibt uns den folgenden Hashwert zurück: "
f"[bold]{hashed_data_hex}[/bold]",
)
if not click.confirm("Fortfahren?", default=True):
return True
console.print("\nDiesen erzeugten Hash werden wir nun in der Blockchain speichern")
console.print(
"\nHierzu muss ein neuer Block 'geschürft' werden. International "
"wird deshalb auch von 'Mining' gesprochen."
)
try:
with console.status(
"Mining: Schürfen eines neuen Blocks mit unseren Daten...", spinner="dots2"
):
block_number = chain.new_block(data_to_hash)
chain.save()
except Exception:
console.print_exception(max_frames=20)
sys.exit(1)
console.print(
f"[green]:heavy_check_mark:[/green] Unsere Daten wurden erfolgreich im Block "
f"mit der Nummer '{block_number}' gespeichert"
)
if not click.confirm("Fortfahren?", default=True):
return True
console.print(
"Diese Information müssen wir noch in der Datenbank ablegen, damit sie "
"für einen späteren Abgleich zur Verfügung steht."
)
try:
stmt = (
sql.update(db.sensor_data)
.where(db.sensor_data.c.Index == sql.bindparam("idx"))
.values(Blockchain_Block_Number=sql.bindparam("bc_num"))
)
with engine.begin() as con:
res = con.execute(stmt, {"idx": max_idx, "bc_num": block_number})
if res.rowcount <= 0:
raise RuntimeError("Database query not successful")
stmt_sel = sql.select(db.sensor_data).where(db.sensor_data.c.Index == max_idx)
db_entry = pl.read_database(
stmt_sel, engine, schema_overrides=db.sensor_data_query_schema
)
table = rich_table_from_db_entry(db_entry, title=f"Sensordaten zu Eintrag {max_idx}")
except Exception:
console.print_exception(max_frames=20)
sys.exit(1)
console.print("Unsere Datenbank enthält zu diesem Eintrag nun folgende Informationen:")
console.print(table)
console.print("Die Nummer des relevanten Blocks ist nun ebenfalls gespeichert.")
if not click.confirm("Fortfahren?", default=True):
return True
try:
py_block = chain.get_block(block_number)
py_block_data = dc.asdict(py_block.as_dataclass())
py_block_data["Timestamp"] = py_block_data["Timestamp"].isoformat()
py_block_json = json.dumps(py_block_data)
py_block_data_hash = py_block_data["Data"]
if py_block_data_hash != hashed_data_hex:
raise RuntimeError("Hash values do not match, but they should. Data corrupted?")
except Exception:
console.print_exception(max_frames=20)
sys.exit(1)
console.print(
"Nun ermitteln wir den Hashwert, der in der Blockchain gespeichert wurde. "
"Die relevante Block-Nummer haben wir bereits erhalten. Sie "
f"lautet: {block_number}"
)
console.print(
"Wir fragen den Block mit dieser Nummer aus der Blockchain ab und "
"erhalten dazu folgende Daten:"
)
console.print_json(py_block_json)
if not click.confirm("Fortfahren?", default=True):
return True
console.print(
f"Der Hashwert der dazugehörigen Daten lautet also demnach: "
f"[bold]{py_block_data_hash}[/bold]"
)
if not click.confirm("Fortfahren?", default=True):
return True
console.print(
"Nun gleichen wir den Hashwert unserer Daten mit dem Hashwert aus der Blockchain ab."
)
console.print(f"Ermittelter Hashwert der Einträge:\t[bold]{hashed_data_hex}[/bold]")
console.print(f"Hashwert der Blockchain:\t\t[bold]{py_block_data_hash}[/bold]")
console.print(
"[green3]Wir sehen, dass beide Werte übereinstimmen. Das bedeutet, dass der Datensatz "
"erfolgreich abgespeichert wurde und für eine zukünftige Prüfung zur Verfügung steht."
)
return False
# %% # %%
# // app start # // app start
def app() -> None: def app() -> bool:
console.rule("[bold]Willkommen beim POLLU-BLOCK-Demonstrator!", style="yellow3") console.rule("[bold]Willkommen beim POLLU-BLOCK-Demonstrator!", style="yellow3")
console.print( console.print(
"Diese Anwendung zeigt Ihnen, wie eine Blockchain als " "Diese Anwendung zeigt Ihnen, wie eine Blockchain als "
@ -280,7 +506,8 @@ def app() -> None:
"Blockchain kann geprüft werden, ob die Einträge der Datenbank geändert wurden.\n" "Blockchain kann geprüft werden, ob die Einträge der Datenbank geändert wurden.\n"
"Die Blockchain verwendet Proof-of-Work (PoW) als Konsensus-Algorithmus." "Die Blockchain verwendet Proof-of-Work (PoW) als Konsensus-Algorithmus."
) )
click.confirm("Fortfahren?", default=True) if not click.confirm("Fortfahren?", default=True):
return True
console.print( console.print(
"\nVor dem Start der Anwendung müssen noch ein paar Dinge im Hintergrund " "\nVor dem Start der Anwendung müssen noch ein paar Dinge im Hintergrund "
@ -288,7 +515,9 @@ def app() -> None:
) )
chain = blockchain_init() chain = blockchain_init()
start_seeding() aborted = start_seeding()
if aborted:
return True
console.rule("\n[bold]Szenario-Auswahl", style="yellow3") console.rule("\n[bold]Szenario-Auswahl", style="yellow3")
console.print("Dieser Demonstrator erlaubt die Wahl zwischen zwei Anwendungsszenarien:") console.print("Dieser Demonstrator erlaubt die Wahl zwischen zwei Anwendungsszenarien:")
@ -306,7 +535,8 @@ def app() -> None:
" Integritätsprüfung wie in Szenario 1 zur Verfügung stehen." " Integritätsprüfung wie in Szenario 1 zur Verfügung stehen."
) )
console.print( console.print(
"\nBitte wählen Sie das gewünschte Anwendungsszenario, indem Sie '1' oder '2' eintippen..." "\nBitte wählen Sie das gewünschte Anwendungsszenario, indem Sie [bold]'1'[/bold] "
"oder [bold]'2'[/bold] eintippen..."
) )
scenario: int = 0 scenario: int = 0
while scenario == 0: while scenario == 0:
@ -321,12 +551,20 @@ def app() -> None:
sys.exit(1) sys.exit(1)
shutdown: bool = False shutdown: bool = False
init_state: bool = True
while not shutdown: while not shutdown:
if scenario == types.ApplicationScenarios.DATA_VALIDATION: if scenario == types.ApplicationScenarios.DATA_VALIDATION:
console.print( console.print(
"[green]:heavy_check_mark:[/green] Sie haben Szenario 1 " "[green]:heavy_check_mark:[/green] Sie haben Szenario 1 "
"'Datenvalidierung' gewählt" "'Datenvalidierung' gewählt"
) )
if not init_state:
if click.confirm(
"Soll der Zufallsgenerator erneut initialisiert werden?", default=False
):
reseed()
init_state = False
scenario_finished: bool = False scenario_finished: bool = False
while not scenario_finished: while not scenario_finished:
@ -340,7 +578,20 @@ def app() -> None:
"[green]:heavy_check_mark:[/green] Sie haben Szenario 2 " "[green]:heavy_check_mark:[/green] Sie haben Szenario 2 "
"'Datengenerierung' gewählt" "'Datengenerierung' gewählt"
) )
raise NotImplementedError("Scenario 2 not implemented yet") if not init_state:
if click.confirm(
"Soll der Zufallsgenerator erneut initialisiert werden?", default=False
):
reseed()
init_state = False
scenario_finished: bool = False
while not scenario_finished:
scenario_2(chain)
scenario_finished = not click.confirm(
"Möchten Sie einen weiteren Datensatz generieren?"
)
shutdown = not click.confirm("Möchten Sie das Szenario wechseln?") shutdown = not click.confirm("Möchten Sie das Szenario wechseln?")
@ -349,14 +600,24 @@ def app() -> None:
elif not shutdown and scenario == types.ApplicationScenarios.DATA_GENERATION: elif not shutdown and scenario == types.ApplicationScenarios.DATA_GENERATION:
scenario = types.ApplicationScenarios.DATA_VALIDATION scenario = types.ApplicationScenarios.DATA_VALIDATION
sys.exit(0) return False
def main() -> None: def main() -> None:
aborted: bool = False
try: try:
app() aborted = app()
except (KeyboardInterrupt, click.exceptions.Abort): except (KeyboardInterrupt, click.exceptions.Abort):
console.print("\n[italic]Die Anwendung wurde durch den Nutzer beendet.") console.print("\n[italic]Die Anwendung wurde durch den Nutzer beendet.")
sys.exit(1)
if aborted:
console.print("\n[italic]Die Anwendung wurde durch den Nutzer beendet.")
sys.exit(1)
console.print("\n[italic]Die Anwendung wurde beendet.")
sys.exit(0)
if __name__ == "__main__": if __name__ == "__main__":