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
+ Metadata-Version: 2.4
2
+ Name: proofchain-cli
3
+ Version: 0.1.0
4
+ Summary: Hash-chained, signed, independently-verifiable audit proofs for any Postgres table of real events.
5
+ Requires-Python: >=3.9
6
+ Requires-Dist: psycopg2-binary
7
+ Requires-Dist: cryptography
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ proofchain = proofchain.cli:main
@@ -0,0 +1 @@
1
+ proofchain