dcl-core 0.3.0__tar.gz

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-0.3.0/LICENSE ADDED
@@ -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,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,90 @@
1
+ # DCL Core
2
+
3
+ Tamper-evident record chain + multi-party consensus primitives for AI agent
4
+ audit systems. Part of the Deterministic Commitment Layer / Leibniz Layer™
5
+ ecosystem by Fronesis Labs.
6
+
7
+ This is a clean-history consolidation: the single-agent chain logic
8
+ previously duplicated inside `dcl-webhook`, and the multi-agent consensus
9
+ logic from `dcl-v2`, unified into one module with two known issues fixed.
10
+
11
+ ## What changed vs the previous implementations
12
+
13
+ ### 1. `ChainState.verify()` now detects content tampering
14
+
15
+ The chain logic previously embedded in `dcl-webhook/dcl_core.py` only
16
+ checked that `prev_hash` pointers linked correctly between rows. It never
17
+ recomputed a row's hash from its own stored fields. That meant a party with
18
+ direct database access could edit `verdict`, `confidence`, or `reason` on
19
+ an existing row — without touching `tx_hash`/`prev_hash` — and `verify()`
20
+ would still report the chain as clean. This defeated the core tamper-evident
21
+ claim.
22
+
23
+ `verify()` here recomputes each row's hash from all of its stored fields
24
+ and compares it against the stored `tx_hash`. Editing any field now breaks
25
+ verification. See `tests/test_fixes.py::test_detects_content_tamper_not_just_link_break`.
26
+
27
+ ### 2. Super-Hash consensus no longer uses XOR
28
+
29
+ `dcl-v2`'s original formula was `H*_t = hash(h_t1 ⊕ h_t2 ⊕ ... ⊕ h_tM)`.
30
+ XOR is commutative and reversible: a party submitting last (or able to
31
+ observe the running combination) can solve for a contribution that forces
32
+ the final Super-Hash to any target value, without breaking any hash
33
+ function — cheap arithmetic forgery, not a cryptographic break.
34
+
35
+ `compute_super_hash()` instead sorts contributions by party ID and hashes
36
+ their length-prefixed concatenation. Order no longer matters (no
37
+ last-mover advantage) and the XOR-forgery trick no longer works. See
38
+ `tests/test_fixes.py::test_super_hash_xor_forgery_no_longer_trivial`.
39
+
40
+ ## Structure
41
+
42
+ ```
43
+ dcl_core/
44
+ ├── chain.py ChainState — append-only chain, content-hash verify()
45
+ └── consensus.py ConsensusRound, compute_super_hash — multi-party consensus
46
+ tests/
47
+ └── test_fixes.py Regression tests for both fixes above
48
+ ```
49
+
50
+ ## Quick start
51
+
52
+ ```bash
53
+ pip install -e .
54
+ python -m pytest tests/ -v
55
+ ```
56
+
57
+ ```python
58
+ from dcl_core import ChainState
59
+
60
+ chain = ChainState("audit.db")
61
+ tx_hash, idx = chain.append(
62
+ verdict="COMMIT", input_hash="0xabc...", policy_hash="0xdef...",
63
+ agent_id="agent-1", reason="policy checks passed", confidence=0.95,
64
+ task_type="generation",
65
+ )
66
+
67
+ clean, bad_index, reason = chain.verify()
68
+ ```
69
+
70
+ ```python
71
+ from dcl_core import ConsensusRound, verify_super_hash
72
+
73
+ round_ = ConsensusRound(expected_parties={"org-a", "org-b", "org-c"})
74
+ round_.submit("org-a", h_a)
75
+ round_.submit("org-b", h_b)
76
+ round_.submit("org-c", h_c)
77
+ super_hash, contributions = round_.seal()
78
+ assert verify_super_hash(contributions, super_hash)
79
+ ```
80
+
81
+ ## Scope
82
+
83
+ This repo publishes the **protocol layer only** — record format, hash-chain
84
+ verification, and consensus combination. Scoring/policy engines (behavioral
85
+ fingerprinting, epistemic validation thresholds, reputation weighting) stay
86
+ in separate, closed modules that build on top of this.
87
+
88
+ ## License
89
+
90
+ Apache License 2.0 — see [LICENSE](LICENSE).
@@ -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"
@@ -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]
@@ -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)
@@ -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}
@@ -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,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ dcl_core/__init__.py
5
+ dcl_core/chain.py
6
+ dcl_core/consensus.py
7
+ dcl_core/seal.py
8
+ dcl_core/verify.py
9
+ dcl_core.egg-info/PKG-INFO
10
+ dcl_core.egg-info/SOURCES.txt
11
+ dcl_core.egg-info/dependency_links.txt
12
+ dcl_core.egg-info/requires.txt
13
+ dcl_core.egg-info/top_level.txt
14
+ tests/test_fixes.py
15
+ tests/test_verify_matches_chain_state.py
@@ -0,0 +1,3 @@
1
+
2
+ [dev]
3
+ pytest>=7.0
@@ -0,0 +1 @@
1
+ dcl_core
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "dcl-core"
7
+ version = "0.3.0"
8
+ description = "Tamper-evident record chain, offline chain verification, and multi-party consensus primitives for AI agent audit systems (DCL / Leibniz Layer protocol)."
9
+ readme = "README.md"
10
+ license = { text = "Apache-2.0" }
11
+ requires-python = ">=3.9"
12
+ dependencies = []
13
+ authors = [{ name = "Fronesis Labs" }]
14
+ keywords = ["mcp", "model-context-protocol", "audit", "hash-chain", "ai-agents", "x402"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: Apache Software License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Security :: Cryptography",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://mcp.fronesislabs.com"
26
+ Repository = "https://github.com/Fronesis-Labs/dcl-core"
27
+ Protocol = "https://github.com/Fronesis-Labs/dcl-sdk/blob/main/PROTOCOL.md"
28
+
29
+ [project.optional-dependencies]
30
+ dev = ["pytest>=7.0"]
31
+
32
+ [tool.setuptools.packages.find]
33
+ include = ["dcl_core*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,125 @@
1
+ """
2
+ Regression tests for the two fixes:
3
+ 1. ChainState.verify() must detect content tampering, not just broken links.
4
+ 2. Super-Hash consensus must resist the XOR-forgeability attack.
5
+
6
+ Plus a coverage test for export(), added when dcl-webhook's GET /chain/export
7
+ turned out to be calling a method that never existed on ChainState.
8
+ """
9
+
10
+ import os
11
+ import tempfile
12
+
13
+ import pytest
14
+
15
+ from dcl_core.chain import ChainState
16
+ from dcl_core.consensus import compute_super_hash, verify_super_hash, ConsensusRound
17
+
18
+
19
+ def make_chain():
20
+ fd, path = tempfile.mkstemp(suffix=".db")
21
+ os.close(fd)
22
+ os.remove(path) # let sqlite create it fresh
23
+ return ChainState(path), path
24
+
25
+
26
+ def test_clean_chain_verifies():
27
+ chain, path = make_chain()
28
+ chain.append("COMMIT", "ih1", "ph1", "agent-1", "ok", 0.95, "test")
29
+ chain.append("COMMIT", "ih2", "ph1", "agent-1", "ok", 0.90, "test")
30
+ clean, bad_idx, reason = chain.verify()
31
+ assert clean is True
32
+ assert bad_idx is None
33
+
34
+
35
+ def test_detects_content_tamper_not_just_link_break():
36
+ """This is the core regression: editing a field WITHOUT touching
37
+ tx_hash/prev_hash used to pass the old verify(). It must now fail."""
38
+ chain, path = make_chain()
39
+ chain.append("NO_COMMIT", "ih1", "ph1", "agent-1", "policy violation", 0.10, "test")
40
+
41
+ # Simulate an attacker with direct DB access flipping the verdict,
42
+ # leaving tx_hash and prev_hash untouched.
43
+ conn = chain._conn
44
+ conn.execute("UPDATE chain SET verdict = 'COMMIT', confidence = 0.99 WHERE idx = 0")
45
+ conn.commit()
46
+
47
+ clean, bad_idx, reason = chain.verify()
48
+ assert clean is False
49
+ assert bad_idx == 0
50
+ assert "recomputed hash" in reason
51
+
52
+
53
+ def test_detects_broken_link():
54
+ chain, path = make_chain()
55
+ chain.append("COMMIT", "ih1", "ph1", "agent-1", "ok", 0.95, "test")
56
+ chain.append("COMMIT", "ih2", "ph1", "agent-1", "ok", 0.90, "test")
57
+
58
+ conn = chain._conn
59
+ conn.execute("UPDATE chain SET prev_hash = 'deadbeef' WHERE idx = 1")
60
+ conn.commit()
61
+
62
+ clean, bad_idx, reason = chain.verify()
63
+ assert clean is False
64
+ assert bad_idx == 1
65
+
66
+
67
+ def test_export_returns_full_chain_in_order():
68
+ chain, path = make_chain()
69
+ chain.append("COMMIT", "ih1", "ph1", "agent-1", "ok", 0.95, "test")
70
+ chain.append("NO_COMMIT", "ih2", "ph1", "agent-2", "policy violation", 0.10, "test")
71
+
72
+ dump = chain.export()
73
+ assert len(dump) == 2
74
+ assert [row["index"] for row in dump] == [0, 1]
75
+ assert dump[0]["agent_id"] == "agent-1"
76
+ assert dump[1]["verdict"] == "NO_COMMIT"
77
+
78
+
79
+ def test_export_matches_get_by_tx_shape():
80
+ chain, path = make_chain()
81
+ tx_hash, _ = chain.append("COMMIT", "ih1", "ph1", "agent-1", "ok", 0.95, "test")
82
+
83
+ exported_row = chain.export()[0]
84
+ fetched_row = chain.get_by_tx(tx_hash)
85
+ assert exported_row == fetched_row
86
+
87
+
88
+ def test_super_hash_order_independent():
89
+ contributions = {"org-a": "h_a", "org-b": "h_b", "org-c": "h_c"}
90
+ h1 = compute_super_hash(contributions)
91
+ h2 = compute_super_hash({"org-c": "h_c", "org-a": "h_a", "org-b": "h_b"})
92
+ assert h1 == h2
93
+
94
+
95
+ def test_super_hash_xor_forgery_no_longer_trivial():
96
+ """Under the old XOR scheme, a party submitting last could pick h_tM
97
+ such that h_t1 ^ h_t2 ^ ... ^ h_tM equals any target value, by solving
98
+ h_tM = target ^ (h_t1 ^ ... ^ h_t(M-1)). Demonstrate that trick no
99
+ longer forges the new (concatenation-based) Super-Hash."""
100
+ h_a = "aa" * 32
101
+ h_b = "bb" * 32
102
+ target = "11" * 32 # attacker's desired forged super-hash (hex string)
103
+
104
+ # The old XOR-forgery move: compute h_c that would XOR the others to `target`.
105
+ def xor_hex(x: str, y: str) -> str:
106
+ return format(int(x, 16) ^ int(y, 16), f"0{len(x)}x")
107
+
108
+ forged_h_c = xor_hex(xor_hex(h_a, h_b), target)
109
+
110
+ # Under the fixed scheme this forged value does not produce `target`.
111
+ forged_super_hash = compute_super_hash({"org-a": h_a, "org-b": h_b, "org-c": forged_h_c})
112
+ assert forged_super_hash != "0x" + target
113
+
114
+
115
+ def test_consensus_round_requires_all_parties_before_sealing():
116
+ round_ = ConsensusRound(expected_parties={"org-a", "org-b"})
117
+ round_.submit("org-a", "h_a")
118
+ assert round_.is_complete is False
119
+ with pytest.raises(RuntimeError):
120
+ round_.seal()
121
+
122
+ round_.submit("org-b", "h_b")
123
+ assert round_.is_complete is True
124
+ super_hash, contributions = round_.seal()
125
+ assert verify_super_hash(contributions, super_hash)
@@ -0,0 +1,98 @@
1
+ """
2
+ Confirms dcl_core.verify.verify_chain() (standalone, offline, dict-based)
3
+ agrees with ChainState.verify() (SQLite-backed, production) on the exact
4
+ same data. If these two ever disagree, cross-language SDK verification
5
+ (TS, and anyone else implementing PROTOCOL.md) silently breaks — this is
6
+ the guard against that.
7
+ """
8
+
9
+ import tempfile
10
+ import os
11
+
12
+ from dcl_core.chain import ChainState
13
+ from dcl_core.verify import verify_chain, recompute_tx_hash, GENESIS
14
+ from dcl_core.seal import format_seal
15
+
16
+
17
+ def _make_chain(n=5):
18
+ fd, path = tempfile.mkstemp(suffix=".db")
19
+ os.close(fd)
20
+ cs = ChainState(db_path=path)
21
+ for i in range(n):
22
+ cs.append(
23
+ verdict="COMMIT" if i % 2 == 0 else "NO_COMMIT",
24
+ input_hash=f"0x{'a' * 63}{i}",
25
+ policy_hash=f"0x{'b' * 63}{i}",
26
+ agent_id=f"agent-{i}",
27
+ reason=f"reason {i}",
28
+ confidence=0.9 + i * 0.001,
29
+ task_type="test",
30
+ drift_context={"environment": "test", "step": i},
31
+ )
32
+ return cs, path
33
+
34
+
35
+ def test_standalone_verify_matches_chain_state_on_clean_chain():
36
+ cs, path = _make_chain(5)
37
+ try:
38
+ clean, bad_index, reason = cs.verify()
39
+ assert clean is True
40
+
41
+ records = cs.export()
42
+ result = verify_chain(records)
43
+ assert result.clean is True
44
+ assert result.bad_index is None
45
+ finally:
46
+ os.remove(path)
47
+
48
+
49
+ def test_recompute_tx_hash_matches_stored_hash_for_every_record():
50
+ cs, path = _make_chain(3)
51
+ try:
52
+ for record in cs.export():
53
+ assert recompute_tx_hash(record) == record["tx_hash"]
54
+ finally:
55
+ os.remove(path)
56
+
57
+
58
+ def test_standalone_verify_catches_tampered_row():
59
+ cs, path = _make_chain(4)
60
+ try:
61
+ records = cs.export()
62
+ records[2]["confidence"] = 0.111111 # tamper without touching tx_hash
63
+
64
+ result = verify_chain(records)
65
+ assert result.clean is False
66
+ assert result.bad_index == 2
67
+ assert "content hash" in result.reason or "recomputed" in result.reason
68
+ finally:
69
+ os.remove(path)
70
+
71
+
72
+ def test_genesis_prev_hash_constant_matches_chain_state():
73
+ assert GENESIS == ChainState.GENESIS
74
+
75
+
76
+ def test_format_seal_basic_shape():
77
+ seal = format_seal(
78
+ tx_hash="0x" + "a" * 64,
79
+ input_hash="0x" + "b" * 64,
80
+ timestamp=1754400000.123456,
81
+ )
82
+ assert seal["seal_text"].startswith("🔒 Verified by Leibniz Layer | Fronesis Labs")
83
+ assert seal["verify_url"] == "https://x402.fronesislabs.com/verify/" + "a" * 64
84
+ assert "2025-08-05" in seal["seal_text"] or "2025" in seal["seal_text"]
85
+
86
+
87
+ def test_format_seal_white_label_omits_network():
88
+ seal = format_seal(
89
+ tx_hash="0x" + "c" * 64,
90
+ input_hash="0x" + "d" * 64,
91
+ timestamp=1754400000.0,
92
+ brand="My Audit Layer",
93
+ verify_base_url="https://example.com/verify",
94
+ network="",
95
+ )
96
+ assert "My Audit Layer" in seal["seal_text"]
97
+ assert "—" not in seal["seal_text"]
98
+ assert seal["verify_url"] == "https://example.com/verify/" + "c" * 64