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 hashlib
import json
import random
import sys
from pathlib import Path
from typing import cast
import click
import dopt_basics.datetime as dopt_dt
import polars as pl
import rich.box
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 constants as const
user_seed: int | None = const.RNG_DEFAULT_SEED
console = Console()
p_base = io.search_folder_path(Path(__file__), "src", return_inclusive=False)
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)}")
# %%
# // common
# // common helper functions
def print_exception(
console: Console,
error_txt: str,
@ -112,6 +115,24 @@ def df_transform_hashing(
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
def blockchain_init() -> blockchain.Blockchain:
console.print("Einlesen der Blockchain...")
@ -126,6 +147,10 @@ def blockchain_init() -> blockchain.Blockchain:
console.print_exception(max_frames=20)
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("Prüfe Hashwerte und korrekte Verkettung...")
try:
@ -143,8 +168,9 @@ def blockchain_init() -> blockchain.Blockchain:
return chain
# // begin of both scenarios
def start_seeding() -> None:
def start_seeding() -> bool:
global user_seed
console.print(
"\nWir arbeiten mit zufälligen Werten. Damit die Ergebnisse trotzdem reproduzierbar "
"sind, nutzen wir einen Initialwert für den Zufallsgenerator, den Sie "
@ -155,7 +181,10 @@ def start_seeding() -> None:
try:
user_input = console.input("[italic]Wählen Sie einen Wert zwischen 1 und 100: ")
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
rng_seed = int(user_input)
except Exception:
@ -163,15 +192,31 @@ def start_seeding() -> None:
sys.exit(1)
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(
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")
max_idx: int = 0
@ -184,7 +229,8 @@ def scenario_1(chain: blockchain.Blockchain) -> None:
sys.exit(1)
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))
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:"
)
console.print(table)
click.confirm("Fortfahren?", default=True)
if not click.confirm("Fortfahren?", default=True):
return True
try:
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"[bold]{hashed_data_hex}[/bold]",
)
click.confirm("Fortfahren?", default=True)
if not click.confirm("Fortfahren?", default=True):
return True
try:
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"lautet: {block_number}"
)
click.confirm("Fortfahren?", default=True)
if not click.confirm("Fortfahren?", default=True):
return True
console.print(
"Wir fragen den Block mit dieser Nummer aus der Blockchain ab und "
"erhalten dazu folgende Daten:"
)
console.print_json(py_block_json)
click.confirm("Fortfahren?", default=True)
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]"
)
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(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 "
"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
def app() -> None:
def app() -> bool:
console.rule("[bold]Willkommen beim POLLU-BLOCK-Demonstrator!", style="yellow3")
console.print(
"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"
"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(
"\nVor dem Start der Anwendung müssen noch ein paar Dinge im Hintergrund "
@ -288,7 +515,9 @@ def app() -> None:
)
chain = blockchain_init()
start_seeding()
aborted = start_seeding()
if aborted:
return True
console.rule("\n[bold]Szenario-Auswahl", style="yellow3")
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."
)
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
while scenario == 0:
@ -321,12 +551,20 @@ def app() -> None:
sys.exit(1)
shutdown: bool = False
init_state: bool = True
while not shutdown:
if scenario == types.ApplicationScenarios.DATA_VALIDATION:
console.print(
"[green]:heavy_check_mark:[/green] Sie haben Szenario 1 "
"'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
while not scenario_finished:
@ -340,7 +578,20 @@ def app() -> None:
"[green]:heavy_check_mark:[/green] Sie haben Szenario 2 "
"'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?")
@ -349,14 +600,24 @@ def app() -> None:
elif not shutdown and scenario == types.ApplicationScenarios.DATA_GENERATION:
scenario = types.ApplicationScenarios.DATA_VALIDATION
sys.exit(0)
return False
def main() -> None:
aborted: bool = False
try:
app()
aborted = app()
except (KeyboardInterrupt, click.exceptions.Abort):
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__":