dcl-core 0.3.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.
dcl_core/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ from .chain import ChainState, TamperDetectedError, sha256hex
2
+ from .consensus import ConsensusRound, compute_super_hash, verify_super_hash
3
+ from .verify import GENESIS, VerifyResult, canonical_content, recompute_tx_hash, verify_chain
4
+ from .seal import Seal, format_seal
5
+
6
+ __all__ = [
7
+ "ChainState",
8
+ "TamperDetectedError",
9
+ "sha256hex",
10
+ "ConsensusRound",
11
+ "compute_super_hash",
12
+ "verify_super_hash",
13
+ "GENESIS",
14
+ "VerifyResult",
15
+ "canonical_content",
16
+ "recompute_tx_hash",
17
+ "verify_chain",
18
+ "Seal",
19
+ "format_seal",
20
+ ]
21
+
22
+ __version__ = "0.3.0"
dcl_core/chain.py ADDED
@@ -0,0 +1,242 @@
1
+ """
2
+ dcl_core.chain — append-only tamper-evident record chain.
3
+
4
+ Fix vs the version previously embedded in dcl-webhook's dcl_core.py:
5
+ - verify() now RECOMPUTES tx_hash from each row's stored content and
6
+ compares it to the stored tx_hash. The old version only checked that
7
+ prev_hash pointers were linked correctly, which meant an attacker with
8
+ direct DB access could edit a row's `verdict`/`confidence`/`reason`
9
+ without touching tx_hash/prev_hash, and verify() would still report the
10
+ chain as clean. That defeated the core "tamper-evident" claim.
11
+ - The timestamp used inside the hashed content is now the SAME value
12
+ stored in the `timestamp` column (previously two separate time.time()
13
+ calls were made, one for hashing and one for storage, making the hash
14
+ permanently unreproducible from stored fields).
15
+
16
+ Patch (this file): added export() — a plain metadata dump of the full
17
+ chain, oldest first. This is read-only access to already-stored fields
18
+ (the same shape get_by_tx() returns), not scoring/policy logic, so it
19
+ belongs in the open protocol layer. webhook_server.py's GET /chain/export
20
+ was calling _chain.export() against a ChainState that never defined it —
21
+ this closes that gap.
22
+ """
23
+
24
+ import hashlib
25
+ import json
26
+ import sqlite3
27
+ import threading
28
+ import time
29
+ from typing import Optional, Tuple
30
+
31
+
32
+ def sha256hex(s: str) -> str:
33
+ return hashlib.sha256(s.encode()).hexdigest()
34
+
35
+
36
+ class TamperDetectedError(Exception):
37
+ """Raised when a stored record's hash does not match its recomputed hash."""
38
+
39
+ def __init__(self, index: int, reason: str):
40
+ self.index = index
41
+ self.reason = reason
42
+ super().__init__(f"Tamper detected at index {index}: {reason}")
43
+
44
+
45
+ class ChainState:
46
+ """SQLite-backed tamper-evident chain. Stores METADATA ONLY — never raw content."""
47
+
48
+ GENESIS = "0" * 64
49
+ HASH_LEN = 64 # full sha256 hex length; do not truncate (previous version cut to 32)
50
+
51
+ def __init__(self, db_path: str = "dcl_chain.db"):
52
+ self._lock = threading.RLock() # same-process thread safety
53
+ self._conn = sqlite3.connect(db_path, timeout=30.0, check_same_thread=False)
54
+ self._conn.execute("PRAGMA journal_mode=WAL")
55
+ self._conn.execute(
56
+ """
57
+ CREATE TABLE IF NOT EXISTS chain (
58
+ idx INTEGER PRIMARY KEY,
59
+ tx_hash TEXT UNIQUE NOT NULL,
60
+ prev_hash TEXT NOT NULL,
61
+ verdict TEXT NOT NULL,
62
+ input_hash TEXT NOT NULL,
63
+ policy_hash TEXT NOT NULL,
64
+ agent_id TEXT NOT NULL,
65
+ reason TEXT NOT NULL,
66
+ confidence REAL NOT NULL,
67
+ task_type TEXT NOT NULL,
68
+ timestamp REAL NOT NULL,
69
+ drift_context TEXT NOT NULL
70
+ )
71
+ """
72
+ )
73
+ self._conn.execute("CREATE INDEX IF NOT EXISTS idx_tx_hash ON chain(tx_hash)")
74
+ self._conn.commit()
75
+
76
+ @staticmethod
77
+ def _content_for_hash(
78
+ idx: int,
79
+ verdict: str,
80
+ input_hash: str,
81
+ policy_hash: str,
82
+ prev_hash: str,
83
+ agent_id: str,
84
+ reason: str,
85
+ confidence: float,
86
+ task_type: str,
87
+ timestamp: float,
88
+ drift_context_json: str,
89
+ ) -> str:
90
+ """Canonical, order-fixed serialization of everything that must be tamper-evident.
91
+
92
+ Every field that can be independently edited in the DB is included here.
93
+ The old version only hashed idx/verdict/input_hash/policy_hash/prev_hash/time —
94
+ agent_id, reason, confidence, task_type, and drift_context were NOT part of the
95
+ hash, so editing any of those fields directly in SQLite was invisible to verify().
96
+ """
97
+ return "|".join(
98
+ [
99
+ str(idx),
100
+ verdict,
101
+ input_hash,
102
+ policy_hash,
103
+ prev_hash,
104
+ agent_id,
105
+ reason,
106
+ f"{confidence:.6f}",
107
+ task_type,
108
+ f"{timestamp:.6f}",
109
+ drift_context_json,
110
+ ]
111
+ )
112
+
113
+ def append(
114
+ self,
115
+ verdict: str,
116
+ input_hash: str,
117
+ policy_hash: str,
118
+ agent_id: str,
119
+ reason: str,
120
+ confidence: float,
121
+ task_type: str,
122
+ drift_context: Optional[dict] = None,
123
+ ) -> Tuple[str, int]:
124
+ # BEGIN IMMEDIATE grabs the SQLite write-lock before the SELECT, not
125
+ # after — this is what actually closes the race, not just the
126
+ # in-process RLock below. mcp_server.py and webhook_server.py are
127
+ # separate PM2 processes sharing this same db file; without this,
128
+ # two near-simultaneous appends (one MCP tool call, one REST call)
129
+ # can both read the same MAX(idx)/prev_hash and then collide on the
130
+ # INSERT (idx is a PRIMARY KEY), crashing the second caller's request
131
+ # instead of just making it wait its turn.
132
+ with self._lock:
133
+ self._conn.execute("BEGIN IMMEDIATE")
134
+ try:
135
+ last = self._conn.execute(
136
+ "SELECT tx_hash FROM chain ORDER BY idx DESC LIMIT 1"
137
+ ).fetchone()
138
+ prev_hash = last[0] if last else self.GENESIS
139
+ new_idx = self._conn.execute(
140
+ "SELECT COALESCE(MAX(idx), -1) + 1 FROM chain"
141
+ ).fetchone()[0]
142
+
143
+ timestamp = time.time()
144
+ drift_context = drift_context or {}
145
+ drift_context_json = json.dumps(drift_context, sort_keys=True)
146
+
147
+ content = self._content_for_hash(
148
+ new_idx, verdict, input_hash, policy_hash, prev_hash,
149
+ agent_id, reason, confidence, task_type, timestamp, drift_context_json,
150
+ )
151
+ tx_hash = "0x" + sha256hex(content) # full 64 hex chars, not truncated
152
+
153
+ self._conn.execute(
154
+ """
155
+ INSERT INTO chain (idx, tx_hash, prev_hash, verdict, input_hash, policy_hash,
156
+ agent_id, reason, confidence, task_type, timestamp, drift_context)
157
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
158
+ """,
159
+ (new_idx, tx_hash, prev_hash, verdict, input_hash, policy_hash,
160
+ agent_id, reason, confidence, task_type, timestamp, drift_context_json),
161
+ )
162
+ self._conn.commit()
163
+ return tx_hash, new_idx
164
+ except Exception:
165
+ self._conn.rollback()
166
+ raise
167
+
168
+ def get_by_tx(self, tx_hash: str) -> Optional[dict]:
169
+ row = self._conn.execute(
170
+ """
171
+ SELECT idx, tx_hash, prev_hash, verdict, input_hash, policy_hash,
172
+ agent_id, reason, confidence, task_type, timestamp, drift_context
173
+ FROM chain WHERE tx_hash = ?
174
+ """,
175
+ (tx_hash,),
176
+ ).fetchone()
177
+ if not row:
178
+ return None
179
+ return self._row_to_dict(row)
180
+
181
+ def export(self) -> list:
182
+ """Full chain dump, oldest first. Metadata only — never raw content.
183
+
184
+ Same field shape as get_by_tx(), for every row in the chain. Read-only
185
+ access to already-stored data; not scoring/policy logic, so it stays
186
+ in the open protocol layer alongside append()/get_by_tx()/verify().
187
+ """
188
+ rows = self._conn.execute(
189
+ """
190
+ SELECT idx, tx_hash, prev_hash, verdict, input_hash, policy_hash,
191
+ agent_id, reason, confidence, task_type, timestamp, drift_context
192
+ FROM chain ORDER BY idx
193
+ """
194
+ ).fetchall()
195
+ return [self._row_to_dict(row) for row in rows]
196
+
197
+ @staticmethod
198
+ def _row_to_dict(row) -> dict:
199
+ return {
200
+ "index": row[0], "tx_hash": row[1], "prev_hash": row[2], "verdict": row[3],
201
+ "input_hash": row[4], "policy_hash": row[5], "agent_id": row[6], "reason": row[7],
202
+ "confidence": row[8], "task_type": row[9], "timestamp": row[10],
203
+ "drift_context": json.loads(row[11]),
204
+ }
205
+
206
+ def verify(self) -> Tuple[bool, Optional[int], Optional[str]]:
207
+ """Full integrity check: link continuity AND content-hash recomputation.
208
+
209
+ Returns (is_clean, first_bad_index_or_None, reason_or_None).
210
+ """
211
+ rows = self._conn.execute(
212
+ """
213
+ SELECT idx, tx_hash, prev_hash, verdict, input_hash, policy_hash,
214
+ agent_id, reason, confidence, task_type, timestamp, drift_context
215
+ FROM chain ORDER BY idx
216
+ """
217
+ ).fetchall()
218
+
219
+ expected_prev = self.GENESIS
220
+ for row in rows:
221
+ (idx, tx_hash, prev_hash, verdict, input_hash, policy_hash,
222
+ agent_id, reason, confidence, task_type, timestamp, drift_context_json) = row
223
+
224
+ # 1. Chain linkage
225
+ if prev_hash != expected_prev:
226
+ return False, idx, "prev_hash does not match preceding tx_hash (link broken)"
227
+
228
+ # 2. Content integrity — recompute and compare
229
+ recomputed_content = self._content_for_hash(
230
+ idx, verdict, input_hash, policy_hash, prev_hash,
231
+ agent_id, reason, confidence, task_type, timestamp, drift_context_json,
232
+ )
233
+ recomputed_hash = "0x" + sha256hex(recomputed_content)
234
+ if recomputed_hash != tx_hash:
235
+ return False, idx, "stored tx_hash does not match recomputed hash of row content (row was edited)"
236
+
237
+ expected_prev = tx_hash
238
+
239
+ return True, None, None
240
+
241
+ def __len__(self):
242
+ return self._conn.execute("SELECT COUNT(*) FROM chain").fetchone()[0]
dcl_core/consensus.py ADDED
@@ -0,0 +1,82 @@
1
+ """
2
+ dcl_core.consensus — cross-organizational Super-Hash consensus (from dcl-v2).
3
+
4
+ Fix vs the original formula H*_t = hash(h_t1 XOR h_t2 XOR ... XOR h_tM):
5
+
6
+ XOR is commutative and reversible. Any party that contributes last (or that
7
+ can observe the running combination before submitting) can solve for a
8
+ value h_tM such that the final H*_t equals whatever they want — without
9
+ breaking any individual party's hash function. That means a dishonest party
10
+ could forge apparent consensus cheaply, via arithmetic on XOR, not via a
11
+ cryptographic break.
12
+
13
+ Fix: combine contributions by hashing their SORTED, length-prefixed
14
+ concatenation instead of XOR-ing them. Sorting makes the result independent
15
+ of submission order (so no party gains an advantage by going last), and
16
+ concatenation removes the XOR-invertibility problem. This is a Merkle-style
17
+ combine, not full tree construction — sufficient for a fixed, known set of M
18
+ parties; swap in a real Merkle tree if M grows large or is dynamic.
19
+ """
20
+
21
+ import hashlib
22
+ from typing import Dict, Tuple
23
+
24
+
25
+ def sha256hex(s: str) -> str:
26
+ return hashlib.sha256(s.encode()).hexdigest()
27
+
28
+
29
+ def compute_super_hash(party_hashes: Dict[str, str]) -> str:
30
+ """Combine per-party hashes into a single cross-organizational commitment.
31
+
32
+ party_hashes: mapping of party_id -> that party's local hash (h_ti) for
33
+ timestep t. Order of insertion does not matter — contributions are
34
+ sorted by party_id before combining, so no party can bias the result by
35
+ controlling submission order.
36
+ """
37
+ if not party_hashes:
38
+ raise ValueError("compute_super_hash requires at least one party contribution")
39
+
40
+ # Sort by party_id for order-independence, then length-prefix each hash
41
+ # to prevent ambiguous concatenation (e.g. "ab"+"c" vs "a"+"bc").
42
+ parts = []
43
+ for party_id in sorted(party_hashes.keys()):
44
+ h = party_hashes[party_id]
45
+ parts.append(f"{len(party_id)}:{party_id}:{len(h)}:{h}")
46
+
47
+ combined = "|".join(parts)
48
+ return "0x" + sha256hex(combined)
49
+
50
+
51
+ def verify_super_hash(party_hashes: Dict[str, str], claimed_super_hash: str) -> bool:
52
+ """Recompute the Super-Hash from party contributions and compare."""
53
+ return compute_super_hash(party_hashes) == claimed_super_hash
54
+
55
+
56
+ class ConsensusRound:
57
+ """Collects per-party hash contributions for one timestep and seals them
58
+ only once all expected parties have submitted (commit-reveal shape:
59
+ collect first, combine once, never recombine on partial data)."""
60
+
61
+ def __init__(self, expected_parties: set):
62
+ self.expected_parties = set(expected_parties)
63
+ self._contributions: Dict[str, str] = {}
64
+ self._sealed_super_hash: str | None = None
65
+
66
+ def submit(self, party_id: str, h_t: str) -> None:
67
+ if self._sealed_super_hash is not None:
68
+ raise RuntimeError("round already sealed; cannot accept further contributions")
69
+ if party_id not in self.expected_parties:
70
+ raise ValueError(f"unexpected party_id: {party_id}")
71
+ self._contributions[party_id] = h_t
72
+
73
+ @property
74
+ def is_complete(self) -> bool:
75
+ return set(self._contributions.keys()) == self.expected_parties
76
+
77
+ def seal(self) -> Tuple[str, Dict[str, str]]:
78
+ if not self.is_complete:
79
+ missing = self.expected_parties - set(self._contributions.keys())
80
+ raise RuntimeError(f"cannot seal: missing contributions from {missing}")
81
+ self._sealed_super_hash = compute_super_hash(self._contributions)
82
+ return self._sealed_super_hash, dict(self._contributions)
dcl_core/seal.py ADDED
@@ -0,0 +1,65 @@
1
+ """
2
+ dcl_core.seal — human-readable verification seal formatting.
3
+
4
+ Pure presentation over already-public fields (tx_hash, input_hash,
5
+ timestamp). No scoring/policy logic, no network call — mirrors
6
+ `formatSeal()` in @fronesis-labs/dcl-sdk (TypeScript) exactly, including
7
+ the white-label options, so other DCL-protocol implementers aren't stuck
8
+ with Fronesis Labs' branding.
9
+ """
10
+
11
+ from datetime import datetime, timezone
12
+ from typing import TypedDict
13
+
14
+
15
+ class Seal(TypedDict):
16
+ seal_text: str
17
+ verify_url: str
18
+
19
+
20
+ DEFAULT_BRAND = "Leibniz Layer | Fronesis Labs"
21
+ DEFAULT_VERIFY_BASE_URL = "https://x402.fronesislabs.com/verify"
22
+ DEFAULT_NETWORK = "Base Mainnet"
23
+
24
+
25
+ def _strip_hex_prefix(s: str) -> str:
26
+ return s[2:] if s.startswith("0x") else s
27
+
28
+
29
+ def _format_sealed_timestamp(unix_seconds: float) -> str:
30
+ return datetime.fromtimestamp(unix_seconds, tz=timezone.utc).strftime(
31
+ "%Y-%m-%d %H:%M UTC"
32
+ )
33
+
34
+
35
+ def format_seal(
36
+ tx_hash: str,
37
+ input_hash: str,
38
+ timestamp: float,
39
+ brand: str = DEFAULT_BRAND,
40
+ verify_base_url: str = DEFAULT_VERIFY_BASE_URL,
41
+ network: str = DEFAULT_NETWORK,
42
+ ) -> Seal:
43
+ """Formats a human-readable "Verified by..." seal.
44
+
45
+ Does not fetch or verify anything itself — call verify_chain() /
46
+ recompute_tx_hash() first if you want an integrity guarantee behind
47
+ the seal you're displaying. Set network="" to omit the network suffix.
48
+ """
49
+ hash_display = _strip_hex_prefix(tx_hash)
50
+ intent_display = _strip_hex_prefix(input_hash)
51
+ sealed = _format_sealed_timestamp(timestamp)
52
+ verify_url = f"{verify_base_url}/{hash_display}"
53
+ network_suffix = f" — {network}" if network else ""
54
+
55
+ seal_text = "\n".join(
56
+ [
57
+ f"🔒 Verified by {brand}",
58
+ f"Hash: {hash_display}",
59
+ f"Intent: {intent_display}",
60
+ f"Sealed: {sealed}{network_suffix}",
61
+ f"Verify: {verify_url}",
62
+ ]
63
+ )
64
+
65
+ return {"seal_text": seal_text, "verify_url": verify_url}
dcl_core/verify.py ADDED
@@ -0,0 +1,90 @@
1
+ """
2
+ dcl_core.verify — standalone, offline chain verification.
3
+
4
+ This is the Python counterpart to `verifyChain()` / `recomputeTxHash()` /
5
+ `canonicalContent()` in `@fronesis-labs/dcl-sdk` (TypeScript). It operates
6
+ on plain dicts (the same shape `ChainState.export()` / `get_by_tx()`
7
+ return) — no SQLite, no network. Anyone holding a chain export (their own,
8
+ or fetched from someone else's server) can independently confirm it wasn't
9
+ tampered with, without trusting the server that handed it to them.
10
+
11
+ Implements PROTOCOL.md §1 (canonical content string), §2 (chain linking),
12
+ §3 (verification algorithm). `canonical_content()` here and
13
+ `ChainState._content_for_hash()` in chain.py must stay byte-identical —
14
+ that invariant is what `tests/test_verify_matches_chain_state.py` checks.
15
+ """
16
+
17
+ import hashlib
18
+ import json
19
+ from dataclasses import dataclass
20
+ from typing import Optional
21
+
22
+ GENESIS = "0" * 64
23
+
24
+
25
+ def sha256hex(s: str) -> str:
26
+ return hashlib.sha256(s.encode()).hexdigest()
27
+
28
+
29
+ def canonical_content(record: dict) -> str:
30
+ """§1 — the exact pipe-delimited string that gets SHA-256 hashed.
31
+
32
+ `record` must have: index, verdict, input_hash, policy_hash, prev_hash,
33
+ agent_id, reason, confidence, task_type, timestamp, drift_context.
34
+ """
35
+ drift_context = record.get("drift_context") or {}
36
+ drift_context_json = json.dumps(drift_context, sort_keys=True)
37
+ return "|".join(
38
+ [
39
+ str(record["index"]),
40
+ record["verdict"],
41
+ record["input_hash"],
42
+ record["policy_hash"],
43
+ record["prev_hash"],
44
+ record["agent_id"],
45
+ record["reason"],
46
+ f"{record['confidence']:.6f}",
47
+ record["task_type"],
48
+ f"{record['timestamp']:.6f}",
49
+ drift_context_json,
50
+ ]
51
+ )
52
+
53
+
54
+ def recompute_tx_hash(record: dict) -> str:
55
+ """§1.2 — recomputes tx_hash for a single record from its stored fields."""
56
+ return "0x" + sha256hex(canonical_content(record))
57
+
58
+
59
+ @dataclass
60
+ class VerifyResult:
61
+ clean: bool
62
+ bad_index: Optional[int] = None
63
+ reason: Optional[str] = None
64
+
65
+
66
+ def verify_chain(records: list) -> VerifyResult:
67
+ """§3 — full verification: chain-link continuity AND per-record
68
+ content-hash recomputation. Catches direct-DB edits that a server's
69
+ own /audit response can't be trusted to admit to.
70
+ """
71
+ sorted_records = sorted(records, key=lambda r: r["index"])
72
+ expected_prev = GENESIS
73
+
74
+ for r in sorted_records:
75
+ if r["prev_hash"] != expected_prev:
76
+ return VerifyResult(
77
+ clean=False,
78
+ bad_index=r["index"],
79
+ reason="prev_hash does not match preceding tx_hash (link broken)",
80
+ )
81
+ recomputed = recompute_tx_hash(r)
82
+ if recomputed != r["tx_hash"]:
83
+ return VerifyResult(
84
+ clean=False,
85
+ bad_index=r["index"],
86
+ reason="stored tx_hash does not match recomputed hash of row content (row was edited)",
87
+ )
88
+ expected_prev = r["tx_hash"]
89
+
90
+ return VerifyResult(clean=True)
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: dcl-core
3
+ Version: 0.3.0
4
+ Summary: Tamper-evident record chain, offline chain verification, and multi-party consensus primitives for AI agent audit systems (DCL / Leibniz Layer protocol).
5
+ Author: Fronesis Labs
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://mcp.fronesislabs.com
8
+ Project-URL: Repository, https://github.com/Fronesis-Labs/dcl-core
9
+ Project-URL: Protocol, https://github.com/Fronesis-Labs/dcl-sdk/blob/main/PROTOCOL.md
10
+ Keywords: mcp,model-context-protocol,audit,hash-chain,ai-agents,x402
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Security :: Cryptography
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7.0; extra == "dev"
22
+ Dynamic: license-file
23
+
24
+ # DCL Core
25
+
26
+ Tamper-evident record chain + multi-party consensus primitives for AI agent
27
+ audit systems. Part of the Deterministic Commitment Layer / Leibniz Layer™
28
+ ecosystem by Fronesis Labs.
29
+
30
+ This is a clean-history consolidation: the single-agent chain logic
31
+ previously duplicated inside `dcl-webhook`, and the multi-agent consensus
32
+ logic from `dcl-v2`, unified into one module with two known issues fixed.
33
+
34
+ ## What changed vs the previous implementations
35
+
36
+ ### 1. `ChainState.verify()` now detects content tampering
37
+
38
+ The chain logic previously embedded in `dcl-webhook/dcl_core.py` only
39
+ checked that `prev_hash` pointers linked correctly between rows. It never
40
+ recomputed a row's hash from its own stored fields. That meant a party with
41
+ direct database access could edit `verdict`, `confidence`, or `reason` on
42
+ an existing row — without touching `tx_hash`/`prev_hash` — and `verify()`
43
+ would still report the chain as clean. This defeated the core tamper-evident
44
+ claim.
45
+
46
+ `verify()` here recomputes each row's hash from all of its stored fields
47
+ and compares it against the stored `tx_hash`. Editing any field now breaks
48
+ verification. See `tests/test_fixes.py::test_detects_content_tamper_not_just_link_break`.
49
+
50
+ ### 2. Super-Hash consensus no longer uses XOR
51
+
52
+ `dcl-v2`'s original formula was `H*_t = hash(h_t1 ⊕ h_t2 ⊕ ... ⊕ h_tM)`.
53
+ XOR is commutative and reversible: a party submitting last (or able to
54
+ observe the running combination) can solve for a contribution that forces
55
+ the final Super-Hash to any target value, without breaking any hash
56
+ function — cheap arithmetic forgery, not a cryptographic break.
57
+
58
+ `compute_super_hash()` instead sorts contributions by party ID and hashes
59
+ their length-prefixed concatenation. Order no longer matters (no
60
+ last-mover advantage) and the XOR-forgery trick no longer works. See
61
+ `tests/test_fixes.py::test_super_hash_xor_forgery_no_longer_trivial`.
62
+
63
+ ## Structure
64
+
65
+ ```
66
+ dcl_core/
67
+ ├── chain.py ChainState — append-only chain, content-hash verify()
68
+ └── consensus.py ConsensusRound, compute_super_hash — multi-party consensus
69
+ tests/
70
+ └── test_fixes.py Regression tests for both fixes above
71
+ ```
72
+
73
+ ## Quick start
74
+
75
+ ```bash
76
+ pip install -e .
77
+ python -m pytest tests/ -v
78
+ ```
79
+
80
+ ```python
81
+ from dcl_core import ChainState
82
+
83
+ chain = ChainState("audit.db")
84
+ tx_hash, idx = chain.append(
85
+ verdict="COMMIT", input_hash="0xabc...", policy_hash="0xdef...",
86
+ agent_id="agent-1", reason="policy checks passed", confidence=0.95,
87
+ task_type="generation",
88
+ )
89
+
90
+ clean, bad_index, reason = chain.verify()
91
+ ```
92
+
93
+ ```python
94
+ from dcl_core import ConsensusRound, verify_super_hash
95
+
96
+ round_ = ConsensusRound(expected_parties={"org-a", "org-b", "org-c"})
97
+ round_.submit("org-a", h_a)
98
+ round_.submit("org-b", h_b)
99
+ round_.submit("org-c", h_c)
100
+ super_hash, contributions = round_.seal()
101
+ assert verify_super_hash(contributions, super_hash)
102
+ ```
103
+
104
+ ## Scope
105
+
106
+ This repo publishes the **protocol layer only** — record format, hash-chain
107
+ verification, and consensus combination. Scoring/policy engines (behavioral
108
+ fingerprinting, epistemic validation thresholds, reputation weighting) stay
109
+ in separate, closed modules that build on top of this.
110
+
111
+ ## License
112
+
113
+ Apache License 2.0 — see [LICENSE](LICENSE).
@@ -0,0 +1,10 @@
1
+ dcl_core/__init__.py,sha256=yWfQ8Qk7Hpis5iOKjGnqomytnNNYkZfvS34_CUvx15k,578
2
+ dcl_core/chain.py,sha256=yYRppU_DHEY7pJddB97VLKb5r3sHFi-BiJtYNasRzXU,9762
3
+ dcl_core/consensus.py,sha256=0DRVL_rzFnDcuwahhR-ZLcRst2gyXWn_NEyc-_BP9Gk,3563
4
+ dcl_core/seal.py,sha256=7Y3hFm35YyNzyxeMpLWXLdOM_t0cfvYHh5Z-UAjtr7M,1990
5
+ dcl_core/verify.py,sha256=Hd1F30fK5Y082xykjigf8mJlhO0SOsAh1h-UPk2SGSQ,3099
6
+ dcl_core-0.3.0.dist-info/licenses/LICENSE,sha256=BpdhKgMEUmVM3aMmLNTPh-ixgnpc98VQ35-ZZ6Fu9n8,742
7
+ dcl_core-0.3.0.dist-info/METADATA,sha256=vq4RIpne75wAydb8fzc8Q13Zs2QClKkZ4Ta36mAXAoI,4372
8
+ dcl_core-0.3.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
9
+ dcl_core-0.3.0.dist-info/top_level.txt,sha256=6CbMQR__jp8WvdTA4Qp-GCmA2TXhwWUcMlgJGUacdFc,9
10
+ dcl_core-0.3.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,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+
17
+ Copyright 2026 Fronesis Labs
@@ -0,0 +1 @@
1
+ dcl_core