proofchain-cli 0.1.0__py3-none-any.whl
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.
proofchain/__init__.py
ADDED
|
File without changes
|
proofchain/cli.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
proofchain.py — generic hash-chained, signed audit-proof CLI.
|
|
4
|
+
|
|
5
|
+
Point it at ANY Postgres table of real events (trades, sales, burns,
|
|
6
|
+
whatever) and get a tamper-evident, independently-verifiable proof log.
|
|
7
|
+
Generalized from omega_revenue_proof.py — same mechanism, config-driven
|
|
8
|
+
instead of hardcoded to one schema.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
export PROOFCHAIN_DSN="postgresql://user@host:port/db"
|
|
12
|
+
export PROOFCHAIN_KEYFILE="/path/to/key.json" # {"secret":[...]} or [...]
|
|
13
|
+
|
|
14
|
+
python3 proofchain.py build \
|
|
15
|
+
--table my_trades \
|
|
16
|
+
--id-col id \
|
|
17
|
+
--columns symbol,pnl_usdt,closed_at \
|
|
18
|
+
--where "status IN ('closed_profit','closed_stop') AND closed_at IS NOT NULL" \
|
|
19
|
+
--order-by closed_at \
|
|
20
|
+
--log ./my_proof.jsonl
|
|
21
|
+
|
|
22
|
+
python3 proofchain.py verify --log ./my_proof.jsonl
|
|
23
|
+
|
|
24
|
+
No table/column names are hardcoded. Nothing here reads or writes
|
|
25
|
+
omega_bank, omega_ledger, or any Omega-specific table unless you pass
|
|
26
|
+
those names explicitly via --table.
|
|
27
|
+
"""
|
|
28
|
+
import os, sys, json, base64, argparse, hashlib
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from datetime import datetime, timezone
|
|
31
|
+
|
|
32
|
+
import psycopg2
|
|
33
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
34
|
+
Ed25519PrivateKey, Ed25519PublicKey
|
|
35
|
+
)
|
|
36
|
+
from cryptography.exceptions import InvalidSignature
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_signer():
|
|
40
|
+
keyfile = os.environ.get("PROOFCHAIN_KEYFILE")
|
|
41
|
+
if not keyfile:
|
|
42
|
+
sys.exit("PROOFCHAIN_KEYFILE env var not set — no placeholder key will be used.")
|
|
43
|
+
data = json.loads(Path(keyfile).read_text())
|
|
44
|
+
raw = bytes(data["secret"][:32]) if isinstance(data, dict) else bytes(data[:32])
|
|
45
|
+
priv = Ed25519PrivateKey.from_private_bytes(raw)
|
|
46
|
+
pub = priv.public_key()
|
|
47
|
+
return priv, pub
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build(args):
|
|
51
|
+
dsn = os.environ.get("PROOFCHAIN_DSN")
|
|
52
|
+
if not dsn:
|
|
53
|
+
sys.exit("PROOFCHAIN_DSN env var not set.")
|
|
54
|
+
|
|
55
|
+
priv, pub = load_signer()
|
|
56
|
+
cols = [c.strip() for c in args.columns.split(",")]
|
|
57
|
+
select_cols = ", ".join([args.id_col] + cols)
|
|
58
|
+
|
|
59
|
+
query = f"SELECT {select_cols} FROM {args.table}"
|
|
60
|
+
if args.where:
|
|
61
|
+
query += f" WHERE {args.where}"
|
|
62
|
+
if args.order_by:
|
|
63
|
+
query += f" ORDER BY {args.order_by} ASC"
|
|
64
|
+
|
|
65
|
+
conn = psycopg2.connect(dsn)
|
|
66
|
+
with conn.cursor() as cur:
|
|
67
|
+
cur.execute(query)
|
|
68
|
+
rows = cur.fetchall()
|
|
69
|
+
conn.close()
|
|
70
|
+
|
|
71
|
+
records = []
|
|
72
|
+
for row in rows:
|
|
73
|
+
rec = {"id": str(row[0])}
|
|
74
|
+
for i, col in enumerate(cols):
|
|
75
|
+
rec[col] = str(row[i + 1]) if row[i + 1] is not None else None
|
|
76
|
+
records.append(rec)
|
|
77
|
+
|
|
78
|
+
log_path = Path(args.log)
|
|
79
|
+
prev_hash = "genesis"
|
|
80
|
+
if log_path.exists() and log_path.stat().st_size > 0:
|
|
81
|
+
last_line = log_path.read_text().strip().splitlines()[-1]
|
|
82
|
+
prev_hash = json.loads(last_line)["entry_hash"]
|
|
83
|
+
|
|
84
|
+
snapshot = {
|
|
85
|
+
"type": "proofchain_snapshot",
|
|
86
|
+
"table": args.table,
|
|
87
|
+
"record_count": len(records),
|
|
88
|
+
"records": records,
|
|
89
|
+
"prev_hash": prev_hash,
|
|
90
|
+
"built_at": datetime.now(timezone.utc).isoformat(),
|
|
91
|
+
}
|
|
92
|
+
msg = json.dumps(snapshot, sort_keys=True, default=str).encode()
|
|
93
|
+
sig = base64.b64encode(priv.sign(msg)).decode()
|
|
94
|
+
entry_hash = hashlib.sha256(msg + sig.encode()).hexdigest()
|
|
95
|
+
snapshot["signature"] = sig
|
|
96
|
+
snapshot["entry_hash"] = entry_hash
|
|
97
|
+
|
|
98
|
+
with open(log_path, "a") as f:
|
|
99
|
+
f.write(json.dumps(snapshot, default=str) + "\n")
|
|
100
|
+
|
|
101
|
+
print(f"Snapshot written. {len(records)} record(s) from '{args.table}'. "
|
|
102
|
+
f"entry_hash={entry_hash[:16]}...")
|
|
103
|
+
print(f"Log: {log_path}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def verify(args):
|
|
107
|
+
priv, pub = load_signer()
|
|
108
|
+
log_path = Path(args.log)
|
|
109
|
+
if not log_path.exists():
|
|
110
|
+
print("No log yet.")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
lines = log_path.read_text().strip().splitlines()
|
|
114
|
+
prev_hash = "genesis"
|
|
115
|
+
ok = True
|
|
116
|
+
for i, line in enumerate(lines):
|
|
117
|
+
record = json.loads(line)
|
|
118
|
+
sig = record.pop("signature")
|
|
119
|
+
claimed_hash = record.pop("entry_hash")
|
|
120
|
+
if record.get("prev_hash") != prev_hash:
|
|
121
|
+
print(f" [BROKEN CHAIN] entry {i}: expected prev_hash={prev_hash}, "
|
|
122
|
+
f"got {record.get('prev_hash')}")
|
|
123
|
+
ok = False
|
|
124
|
+
msg = json.dumps(record, sort_keys=True, default=str).encode()
|
|
125
|
+
try:
|
|
126
|
+
pub.verify(base64.b64decode(sig), msg)
|
|
127
|
+
except InvalidSignature:
|
|
128
|
+
print(f" [BAD SIGNATURE] entry {i}")
|
|
129
|
+
ok = False
|
|
130
|
+
recomputed = hashlib.sha256(msg + sig.encode()).hexdigest()
|
|
131
|
+
if recomputed != claimed_hash:
|
|
132
|
+
print(f" [HASH MISMATCH] entry {i}")
|
|
133
|
+
ok = False
|
|
134
|
+
prev_hash = claimed_hash
|
|
135
|
+
|
|
136
|
+
print("VERIFIED: chain intact, all signatures valid" if ok
|
|
137
|
+
else "FAILED: tampering or corruption detected")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def main():
|
|
141
|
+
parser = argparse.ArgumentParser()
|
|
142
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
143
|
+
|
|
144
|
+
b = sub.add_parser("build")
|
|
145
|
+
b.add_argument("--table", required=True)
|
|
146
|
+
b.add_argument("--id-col", default="id")
|
|
147
|
+
b.add_argument("--columns", required=True, help="comma-separated column list")
|
|
148
|
+
b.add_argument("--where", default="")
|
|
149
|
+
b.add_argument("--order-by", default="")
|
|
150
|
+
b.add_argument("--log", required=True)
|
|
151
|
+
|
|
152
|
+
v = sub.add_parser("verify")
|
|
153
|
+
v.add_argument("--log", required=True)
|
|
154
|
+
|
|
155
|
+
args = parser.parse_args()
|
|
156
|
+
if args.cmd == "build":
|
|
157
|
+
build(args)
|
|
158
|
+
elif args.cmd == "verify":
|
|
159
|
+
verify(args)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
main()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
proofchain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
proofchain/cli.py,sha256=DnrFwq6RbHW2nwxH-8--ce9B2L0LAl0Y_9hsONz_E9c,5380
|
|
3
|
+
proofchain_cli-0.1.0.dist-info/METADATA,sha256=B6hXgtlt6duukl7VChp1VZxE2_Inkw7JEhhr6xTGAS4,248
|
|
4
|
+
proofchain_cli-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
5
|
+
proofchain_cli-0.1.0.dist-info/entry_points.txt,sha256=awpU-c-OPqffa2YPf7l2i2-tM8_tTZ0-LHrnNYUhTi8,51
|
|
6
|
+
proofchain_cli-0.1.0.dist-info/top_level.txt,sha256=CjzV6DLfQpMhkPqBHT1JUhkCjammZ8yBSMzXHOcoxcw,11
|
|
7
|
+
proofchain_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
proofchain
|