Skip to content

Commit 50135af

Browse files
createkrxr
andauthored
feat: temporal entropy profile validation with rolling history (#19)
* feat(attestation): add temporal entropy consistency review and 10-snapshot history * fix(security/tests): hide temporal review internals and add stable integrated_node shim --------- Co-authored-by: xr <xr@xrdeMac-mini-2.local>
1 parent 080714f commit 50135af

3 files changed

Lines changed: 277 additions & 1 deletion

File tree

integrated_node.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Test/import shim for the integrated RustChain node module.
2+
3+
This provides a stable import name (`integrated_node`) for tests while the
4+
actual implementation file keeps its versioned filename.
5+
"""
6+
7+
from importlib.util import spec_from_file_location, module_from_spec
8+
from pathlib import Path
9+
10+
_TARGET = Path(__file__).resolve().parent / "node" / "rustchain_v2_integrated_v2.2.1_rip200.py"
11+
_spec = spec_from_file_location("rustchain_integrated_impl", _TARGET)
12+
_mod = module_from_spec(_spec)
13+
assert _spec and _spec.loader
14+
_spec.loader.exec_module(_mod)
15+
16+
# Re-export public symbols
17+
for _name in dir(_mod):
18+
if _name.startswith("__"):
19+
continue
20+
globals()[_name] = getattr(_mod, _name)

node/rustchain_v2_integrated_v2.2.1_rip200.py

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
RustChain v2 - Integrated Server
44
Includes RIP-0005 (Epoch Rewards), RIP-0008 (Withdrawals), RIP-0009 (Finality)
55
"""
6-
import os, time, json, secrets, hashlib, hmac, sqlite3, base64, struct, uuid, glob, logging, sys, binascii, math, re
6+
import os, time, json, secrets, hashlib, hmac, sqlite3, base64, struct, uuid, glob, logging, sys, binascii, math, re, statistics
77
import ipaddress
88
from urllib.parse import urlparse
99
from flask import Flask, request, jsonify, g, send_from_directory, send_file, abort
@@ -807,6 +807,7 @@ def init_db():
807807
c.execute("CREATE TABLE IF NOT EXISTS epoch_state (epoch INTEGER PRIMARY KEY, accepted_blocks INTEGER DEFAULT 0, finalized INTEGER DEFAULT 0)")
808808
c.execute("CREATE TABLE IF NOT EXISTS epoch_enroll (epoch INTEGER, miner_pk TEXT, weight REAL, PRIMARY KEY (epoch, miner_pk))")
809809
c.execute("CREATE TABLE IF NOT EXISTS balances (miner_pk TEXT PRIMARY KEY, balance_rtc REAL DEFAULT 0)")
810+
ensure_fingerprint_history_table(c)
810811

811812
# Pending transfers (2-phase commit)
812813
# NOTE: Production DBs may already have a different balances schema; this table is additive.
@@ -1116,6 +1117,7 @@ def record_attestation_success(miner: str, device: dict, fingerprint_passed: boo
11161117
INSERT OR REPLACE INTO miner_attest_recent (miner, ts_ok, device_family, device_arch, entropy_score, fingerprint_passed, source_ip)
11171118
VALUES (?, ?, ?, ?, ?, ?, ?)
11181119
""", (miner, now, device.get("device_family", device.get("family", "unknown")), device.get("device_arch", device.get("arch", "unknown")), 0.0, 1 if fingerprint_passed else 0, source_ip))
1120+
_ = append_fingerprint_snapshot(conn, miner, fingerprint if isinstance(fingerprint, dict) else {}, now)
11191121
conn.commit()
11201122

11211123
# RIP-201: Record fleet immune system signals
@@ -1127,6 +1129,155 @@ def record_attestation_success(miner: str, device: dict, fingerprint_passed: boo
11271129
print(f"[RIP-201] Fleet signal recording warning: {_fe}")
11281130
# Auto-induct to Hall of Rust
11291131
auto_induct_to_hall(miner, device)
1132+
1133+
1134+
TEMPORAL_HISTORY_LIMIT = 10
1135+
TEMPORAL_DRIFT_BANDS = {
1136+
"clock_drift_cv": (0.0005, 0.35),
1137+
"thermal_variance": (0.05, 25.0),
1138+
"jitter_cv": (0.0001, 0.50),
1139+
"cache_hierarchy_ratio": (1.10, 20.0),
1140+
}
1141+
1142+
1143+
def ensure_fingerprint_history_table(conn):
1144+
conn.execute(
1145+
"""
1146+
CREATE TABLE IF NOT EXISTS miner_fingerprint_history (
1147+
id INTEGER PRIMARY KEY AUTOINCREMENT,
1148+
miner TEXT NOT NULL,
1149+
ts INTEGER NOT NULL,
1150+
profile_json TEXT NOT NULL
1151+
)
1152+
"""
1153+
)
1154+
conn.execute("CREATE INDEX IF NOT EXISTS idx_mfh_miner_ts ON miner_fingerprint_history(miner, ts DESC)")
1155+
1156+
1157+
def extract_temporal_profile(fingerprint: dict) -> dict:
1158+
checks = (fingerprint or {}).get("checks", {}) if isinstance(fingerprint, dict) else {}
1159+
1160+
def _check_data(name):
1161+
item = checks.get(name, {})
1162+
if isinstance(item, dict):
1163+
data = item.get("data", {})
1164+
return data if isinstance(data, dict) else {}
1165+
return {}
1166+
1167+
clock = _check_data("clock_drift")
1168+
thermal = _check_data("thermal_entropy") or _check_data("thermal_drift")
1169+
jitter = _check_data("instruction_jitter")
1170+
cache = _check_data("cache_timing")
1171+
1172+
return {
1173+
"clock_drift_cv": float(clock.get("cv", 0.0) or 0.0),
1174+
"thermal_variance": float(thermal.get("variance", 0.0) or 0.0),
1175+
"jitter_cv": float(jitter.get("cv", 0.0) or jitter.get("stddev_ns", 0.0) or 0.0),
1176+
"cache_hierarchy_ratio": float(cache.get("hierarchy_ratio", 0.0) or 0.0),
1177+
}
1178+
1179+
1180+
def append_fingerprint_snapshot(conn, miner: str, fingerprint: dict, now: int) -> list:
1181+
ensure_fingerprint_history_table(conn)
1182+
profile = extract_temporal_profile(fingerprint)
1183+
conn.execute(
1184+
"INSERT INTO miner_fingerprint_history (miner, ts, profile_json) VALUES (?, ?, ?)",
1185+
(miner, now, json.dumps(profile, separators=(",", ":"))),
1186+
)
1187+
conn.execute(
1188+
"""
1189+
DELETE FROM miner_fingerprint_history
1190+
WHERE miner = ? AND id NOT IN (
1191+
SELECT id FROM miner_fingerprint_history
1192+
WHERE miner = ?
1193+
ORDER BY ts DESC, id DESC
1194+
LIMIT ?
1195+
)
1196+
""",
1197+
(miner, miner, TEMPORAL_HISTORY_LIMIT),
1198+
)
1199+
rows = conn.execute(
1200+
"SELECT ts, profile_json FROM miner_fingerprint_history WHERE miner = ? ORDER BY ts ASC, id ASC",
1201+
(miner,),
1202+
).fetchall()
1203+
seq = []
1204+
for ts, profile_json in rows:
1205+
try:
1206+
seq.append({"ts": int(ts), "profile": json.loads(profile_json or "{}")})
1207+
except Exception:
1208+
continue
1209+
return seq
1210+
1211+
1212+
def fetch_miner_fingerprint_sequence(conn, miner: str) -> list:
1213+
ensure_fingerprint_history_table(conn)
1214+
rows = conn.execute(
1215+
"SELECT ts, profile_json FROM miner_fingerprint_history WHERE miner = ? ORDER BY ts ASC, id ASC",
1216+
(miner,),
1217+
).fetchall()
1218+
out = []
1219+
for ts, profile_json in rows:
1220+
try:
1221+
out.append({"ts": int(ts), "profile": json.loads(profile_json or "{}")})
1222+
except Exception:
1223+
continue
1224+
return out
1225+
1226+
1227+
def validate_temporal_consistency(sequence: list, current_profile: dict = None) -> dict:
1228+
samples = list(sequence or [])
1229+
if current_profile is not None:
1230+
samples.append({"ts": int(time.time()), "profile": current_profile})
1231+
if len(samples) < 3:
1232+
return {
1233+
"score": 1.0,
1234+
"review_flag": False,
1235+
"reason": "insufficient_history",
1236+
"flags": [],
1237+
"check_scores": {},
1238+
}
1239+
1240+
flags = []
1241+
check_scores = {}
1242+
for metric, (low, high) in TEMPORAL_DRIFT_BANDS.items():
1243+
values = []
1244+
for s in samples:
1245+
p = s.get("profile", {}) if isinstance(s, dict) else {}
1246+
if isinstance(p, dict):
1247+
v = float(p.get(metric, 0.0) or 0.0)
1248+
if v > 0:
1249+
values.append(v)
1250+
1251+
if len(values) < 3:
1252+
check_scores[metric] = 1.0
1253+
continue
1254+
1255+
avg = sum(values) / len(values)
1256+
spread = statistics.pstdev(values)
1257+
rel_var = spread / max(abs(avg), 1e-9)
1258+
1259+
score = 1.0
1260+
if rel_var < 0.01:
1261+
flags.append(f"frozen_profile:{metric}")
1262+
score = min(score, 0.2)
1263+
if rel_var > 0.8:
1264+
flags.append(f"noisy_profile:{metric}")
1265+
score = min(score, 0.3)
1266+
if avg < low or avg > high:
1267+
flags.append(f"drift_out_of_band:{metric}")
1268+
score = min(score, 0.4)
1269+
1270+
check_scores[metric] = score
1271+
1272+
score = sum(check_scores.values()) / max(len(check_scores), 1)
1273+
review_flag = any(f.startswith("frozen_profile") or f.startswith("noisy_profile") or f.startswith("drift_out_of_band") for f in flags)
1274+
return {
1275+
"score": round(score, 4),
1276+
"review_flag": review_flag,
1277+
"reason": "temporal_review_required" if review_flag else "temporal_consistent",
1278+
"flags": flags,
1279+
"check_scores": check_scores,
1280+
}
11301281
# =============================================================================
11311282
# FINGERPRINT VALIDATION (RIP-PoA Anti-Emulation)
11321283
# =============================================================================
@@ -2130,6 +2281,13 @@ def submit_attestation():
21302281
# Record successful attestation (with fingerprint status)
21312282
record_attestation_success(miner, device, fingerprint_passed, client_ip, signals=signals, fingerprint=fingerprint)
21322283

2284+
temporal_review = {"score": 1.0, "review_flag": False, "reason": "insufficient_history", "flags": [], "check_scores": {}}
2285+
try:
2286+
with sqlite3.connect(DB_PATH) as tconn:
2287+
temporal_review = validate_temporal_consistency(fetch_miner_fingerprint_sequence(tconn, miner))
2288+
except Exception as _te:
2289+
print(f"[TEMPORAL] Warning: {_te}")
2290+
21332291
# Update warthog_bonus in attestation record
21342292
if warthog_bonus > 1.0:
21352293
try:
@@ -2159,6 +2317,10 @@ def submit_attestation():
21592317
enroll_weight = 0.000000001
21602318
else:
21612319
enroll_weight = hw_weight
2320+
2321+
# Issue #19 temporal consistency only sets a review flag (no hard-fail).
2322+
if temporal_review.get("review_flag"):
2323+
app.logger.warning(f"[TEMPORAL-REVIEW] {miner[:20]}... flags={temporal_review.get('flags', [])}")
21622324

21632325
miner_id = data.get("miner_id", miner)
21642326

@@ -2210,6 +2372,7 @@ def submit_attestation():
22102372
"status": "accepted",
22112373
"device": device,
22122374
"fingerprint_passed": fingerprint_passed,
2375+
"temporal_review_flag": bool(temporal_review.get("review_flag")),
22132376
"macs_recorded": len(macs) if macs else 0,
22142377
"warthog_bonus": warthog_bonus
22152378
})
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import sqlite3
2+
3+
import integrated_node
4+
5+
6+
def _seq(values, key):
7+
return [{"ts": i, "profile": {key: v}} for i, v in enumerate(values, start=1)]
8+
9+
10+
def test_fingerprint_history_keeps_last_10_snapshots(tmp_path):
11+
db_path = tmp_path / "temporal.db"
12+
with sqlite3.connect(db_path) as conn:
13+
for i in range(12):
14+
fp = {
15+
"checks": {
16+
"clock_drift": {"data": {"cv": 0.02 + i * 0.001}},
17+
"thermal_entropy": {"data": {"variance": 2.0 + i * 0.1}},
18+
"instruction_jitter": {"data": {"cv": 0.04 + i * 0.001}},
19+
"cache_timing": {"data": {"hierarchy_ratio": 3.0 + i * 0.05}},
20+
}
21+
}
22+
integrated_node.append_fingerprint_snapshot(conn, "miner-a", fp, 1_000 + i)
23+
24+
rows = conn.execute(
25+
"SELECT ts FROM miner_fingerprint_history WHERE miner=? ORDER BY ts ASC",
26+
("miner-a",),
27+
).fetchall()
28+
29+
assert len(rows) == 10
30+
assert rows[0][0] == 1_002
31+
assert rows[-1][0] == 1_011
32+
33+
34+
def test_validate_temporal_consistency_real_sequence_passes():
35+
seq = []
36+
for i, cv in enumerate([0.015, 0.020, 0.018, 0.022, 0.019, 0.017], start=1):
37+
seq.append(
38+
{
39+
"ts": i,
40+
"profile": {
41+
"clock_drift_cv": cv,
42+
"thermal_variance": 2.0 + (i % 3) * 0.2,
43+
"jitter_cv": 0.04 + (i % 2) * 0.004,
44+
"cache_hierarchy_ratio": 3.2 + (i % 2) * 0.1,
45+
},
46+
}
47+
)
48+
49+
out = integrated_node.validate_temporal_consistency(seq)
50+
assert out["review_flag"] is False
51+
assert out["score"] >= 0.9
52+
53+
54+
def test_validate_temporal_consistency_frozen_sequence_flagged():
55+
seq = []
56+
for i in range(1, 7):
57+
seq.append(
58+
{
59+
"ts": i,
60+
"profile": {
61+
"clock_drift_cv": 0.02,
62+
"thermal_variance": 2.5,
63+
"jitter_cv": 0.03,
64+
"cache_hierarchy_ratio": 3.4,
65+
},
66+
}
67+
)
68+
69+
out = integrated_node.validate_temporal_consistency(seq)
70+
assert out["review_flag"] is True
71+
assert any(flag.startswith("frozen_profile") for flag in out["flags"])
72+
73+
74+
def test_validate_temporal_consistency_noisy_sequence_flagged():
75+
seq = []
76+
noisy_clock = [0.002, 0.25, 0.004, 0.29, 0.003, 0.27]
77+
noisy_thermal = [0.1, 18.0, 0.2, 16.0, 0.15, 20.0]
78+
for i, (cv, thermal) in enumerate(zip(noisy_clock, noisy_thermal), start=1):
79+
seq.append(
80+
{
81+
"ts": i,
82+
"profile": {
83+
"clock_drift_cv": cv,
84+
"thermal_variance": thermal,
85+
"jitter_cv": 0.02 if i % 2 else 0.3,
86+
"cache_hierarchy_ratio": 3.0 if i % 2 else 9.0,
87+
},
88+
}
89+
)
90+
91+
out = integrated_node.validate_temporal_consistency(seq)
92+
assert out["review_flag"] is True
93+
assert any(flag.startswith("noisy_profile") for flag in out["flags"])

0 commit comments

Comments
 (0)