primust-artifact-core 1.0.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.
@@ -0,0 +1,22 @@
1
+ node_modules/
2
+ dist/
3
+ .next/
4
+ __pycache__/
5
+ *.egg-info/
6
+ .venv/
7
+ .env
8
+ .env.local
9
+ .env.*.local
10
+ *.pyc
11
+ .turbo/
12
+ *.pem
13
+ *.key
14
+ .DS_Store
15
+ .claude/
16
+ .claude.json
17
+ .claude.json.backup
18
+ .pytest_cache/
19
+ *.db
20
+ *.db-shm
21
+ *.db-wal
22
+ .vercel
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: primust-artifact-core
3
+ Version: 1.0.0
4
+ Summary: Canonical JSON, commitment hashing, and Ed25519 signing primitives for Primust.
5
+ Project-URL: Homepage, https://primust.com
6
+ Project-URL: Documentation, https://docs.primust.com
7
+ Author-email: "Primust, Inc." <eng@primust.com>
8
+ License-Expression: LicenseRef-Proprietary
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Security :: Cryptography
16
+ Requires-Python: >=3.11
17
+ Requires-Dist: pynacl>=1.5.0
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "primust-artifact-core"
7
+ version = "1.0.0"
8
+ description = "Canonical JSON, commitment hashing, and Ed25519 signing primitives for Primust."
9
+ requires-python = ">=3.11"
10
+ license = "LicenseRef-Proprietary"
11
+ authors = [{ name = "Primust, Inc.", email = "eng@primust.com" }]
12
+ classifiers = [
13
+ "Development Status :: 5 - Production/Stable",
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Topic :: Security :: Cryptography",
20
+ ]
21
+ dependencies = [
22
+ "pynacl>=1.5.0",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://primust.com"
27
+ Documentation = "https://docs.primust.com"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/primust_artifact_core"]
31
+
32
+ [tool.pytest.ini_options]
33
+ testpaths = ["tests"]
@@ -0,0 +1,31 @@
1
+ """primust-artifact-core — Canonical JSON, hashing, signing (Python mirror)"""
2
+
3
+ from primust_artifact_core.canonical import canonical
4
+ from primust_artifact_core.signing import generate_key_pair, sign, verify, rotate_key
5
+ from primust_artifact_core.types import SignerRecord, SignatureEnvelope
6
+ from primust_artifact_core.validate_artifact import validate_artifact, ValidationError, ValidationResult
7
+ from primust_artifact_core.commitment import (
8
+ commit,
9
+ commit_output,
10
+ build_commitment_root,
11
+ select_proof_level,
12
+ ZK_IS_BLOCKING,
13
+ )
14
+
15
+ __all__ = [
16
+ "canonical",
17
+ "generate_key_pair",
18
+ "sign",
19
+ "verify",
20
+ "rotate_key",
21
+ "SignerRecord",
22
+ "SignatureEnvelope",
23
+ "validate_artifact",
24
+ "ValidationError",
25
+ "ValidationResult",
26
+ "commit",
27
+ "commit_output",
28
+ "build_commitment_root",
29
+ "select_proof_level",
30
+ "ZK_IS_BLOCKING",
31
+ ]
@@ -0,0 +1,84 @@
1
+ """Primust Canonical JSON Serialization (Python mirror).
2
+
3
+ Produces deterministic JSON output with recursively sorted keys
4
+ and no whitespace. Two structurally identical objects always produce
5
+ the same string regardless of key insertion order.
6
+
7
+ Rules:
8
+ - Object keys sorted lexicographically at every nesting depth
9
+ - Array element order preserved (never sorted)
10
+ - No whitespace (no spaces, no newlines, no indentation)
11
+ - Only JSON-native types accepted: str, int, float, bool, None, dict, list
12
+ - Non-JSON-native types (datetime, bytes, etc.) → TypeError
13
+
14
+ Reference: schemas/golden/canonical_vectors.json
15
+ Quarantine: Q1 (top-level-only sort), Q6 (no-sort), Q8 (default=str coercion)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import math
22
+ from typing import Any
23
+
24
+ # Allowed JSON-native types (no default=str, no silent coercion — Q8 quarantine)
25
+ _JSON_NATIVE = (str, int, float, bool, type(None), dict, list)
26
+
27
+
28
+ def canonical(value: Any) -> str:
29
+ """Serialize a value to canonical JSON with recursive key sorting.
30
+
31
+ Raises:
32
+ TypeError: If the value contains non-JSON-native types.
33
+ """
34
+ return _serialize(value)
35
+
36
+
37
+ def _serialize(value: Any) -> str:
38
+ if value is None:
39
+ return "null"
40
+
41
+ if isinstance(value, bool):
42
+ # Must check bool before int (bool is subclass of int in Python)
43
+ return "true" if value else "false"
44
+
45
+ if isinstance(value, int):
46
+ return str(value)
47
+
48
+ if isinstance(value, float):
49
+ if math.isnan(value) or math.isinf(value):
50
+ raise TypeError(
51
+ f"canonical: cannot serialize {value} (NaN/Infinity are not valid JSON)"
52
+ )
53
+ return json.dumps(value)
54
+
55
+ if isinstance(value, str):
56
+ return json.dumps(value, ensure_ascii=False)
57
+
58
+ if isinstance(value, list):
59
+ return _serialize_array(value)
60
+
61
+ if isinstance(value, dict):
62
+ return _serialize_object(value)
63
+
64
+ # Everything else is rejected (Q8: no default=str coercion)
65
+ raise TypeError(
66
+ f"canonical: unsupported type {type(value).__name__}. "
67
+ "Only str, int, float, bool, None, dict, list are accepted."
68
+ )
69
+
70
+
71
+ def _serialize_object(obj: dict) -> str:
72
+ pairs: list[str] = []
73
+ for key in sorted(obj.keys()):
74
+ if not isinstance(key, str):
75
+ raise TypeError(
76
+ f"canonical: dict keys must be strings, got {type(key).__name__}"
77
+ )
78
+ pairs.append(f"{json.dumps(key, ensure_ascii=False)}:{_serialize(obj[key])}")
79
+ return "{" + ",".join(pairs) + "}"
80
+
81
+
82
+ def _serialize_array(arr: list) -> str:
83
+ elements = [_serialize(item) for item in arr]
84
+ return "[" + ",".join(elements) + "]"
@@ -0,0 +1,459 @@
1
+ """
2
+ Primust Artifact Core — Commitment Layer (P6-A) — Python mirror
3
+
4
+ SHA-256 (default) and Poseidon2 (opt-in via PRIMUST_COMMITMENT_ALGORITHM=poseidon2) commitments.
5
+ Poseidon2 uses a pure Python implementation — opt-in only until an audited reference
6
+ (e.g. Barretenberg) is validated.
7
+
8
+ PRIVACY INVARIANT: Raw content NEVER leaves the customer environment.
9
+ Only the commitment hash transits to Primust API.
10
+ """
11
+
12
+ import hashlib
13
+ import os
14
+ from typing import List, Optional, Tuple
15
+
16
+ # ── BN254 Field ──
17
+
18
+ BN254_P = 21888242871839275222246405745257275088548364400416034343698204186575808495617
19
+
20
+ ZK_IS_BLOCKING = False
21
+
22
+ # ── Poseidon2 Constants (BN254, t=4, d=5, 4+56+4 rounds) ──
23
+
24
+ MAT_DIAG4_M_1 = [
25
+ 0x10dc6e9c006ea38b04b1e03b4bd9490c0d03f98929ca1d7fb56821fd19d3b6e7,
26
+ 0x0c28145b6a44df3e0149b3d0a30b3bb599df9756d4dd9b84a86b38cfb45a740b,
27
+ 0x00544b8338791518b2c7645a50392798b21f75bb60e3596170067d00141cac15,
28
+ 0x222c01175718386f2e2e82eb122789e352e105a3b8fa852613bc534433ee428b,
29
+ ]
30
+
31
+ # Full round constants: 4 beginning + 56 partial (first element only) + 4 ending
32
+ RC_FULL_BEGIN = [
33
+ [
34
+ 0x19b849f69450b06848da1d39bd5e4a4302bb86744edc26238b0878e269ed23e5,
35
+ 0x265ddfe127dd51bd7239347b758f0a1320eb2cc7450acc1dad47f80c8dcf34d6,
36
+ 0x199750ec472f1809e0f66a545e1e51624108ac845015c2aa3dfc36bab497d8aa,
37
+ 0x157ff3fe65ac7208110f06a5f74302b14d743ea25067f0ffd032f787c7f1cdf8,
38
+ ],
39
+ [
40
+ 0x2e49c43c4569dd9c5fd35ac45fca33f10b15c590692f8beefe18f4896ac94902,
41
+ 0x0e35fb89981890520d4aef2b6d6506c3cb2f0b6973c24fa82731345ffa2d1f1e,
42
+ 0x251ad47cb15c4f1105f109ae5e944f1ba9d9e7806d667ffec6fe723002e0b996,
43
+ 0x13da07dc64d428369873e97160234641f8beb56fdd05e5f3563fa39d9c22df4e,
44
+ ],
45
+ [
46
+ 0x0c009b84e650e6d23dc00c7dccef7483a553939689d350cd46e7b89055fd4738,
47
+ 0x011f16b1c63a854f01992e3956f42d8b04eb650c6d535eb0203dec74befdca06,
48
+ 0x0ed69e5e383a688f209d9a561daa79612f3f78d0467ad45485df07093f367549,
49
+ 0x04dba94a7b0ce9e221acad41472b6bbe3aec507f5eb3d33f463672264c9f789b,
50
+ ],
51
+ [
52
+ 0x0a3f2637d840f3a16eb094271c9d237b6036757d4bb50bf7ce732ff1d4fa28e8,
53
+ 0x259a666f129eea198f8a1c502fdb38fa39b1f075569564b6e54a485d1182323f,
54
+ 0x28bf7459c9b2f4c6d8e7d06a4ee3a47f7745d4271038e5157a32fdf7ede0d6a1,
55
+ 0x0a1ca941f057037526ea200f489be8d4c37c85bbcce6a2aeec91bd6941432447,
56
+ ],
57
+ ]
58
+
59
+ RC_PARTIAL = [
60
+ 0x0c6f8f958be0e93053d7fd4fc54512855535ed1539f051dcb43a26fd926361cf,
61
+ 0x123106a93cd17578d426e8128ac9d90aa9e8a00708e296e084dd57e69caaf811,
62
+ 0x26e1ba52ad9285d97dd3ab52f8e840085e8fa83ff1e8f1877b074867cd2dee75,
63
+ 0x1cb55cad7bd133de18a64c5c47b9c97cbe4d8b7bf9e095864471537e6a4ae2c5,
64
+ 0x1dcd73e46acd8f8e0e2c7ce04bde7f6d2a53043d5060a41c7143f08e6e9055d0,
65
+ 0x011003e32f6d9c66f5852f05474a4def0cda294a0eb4e9b9b12b9bb4512e5574,
66
+ 0x2b1e809ac1d10ab29ad5f20d03a57dfebadfe5903f58bafed7c508dd2287ae8c,
67
+ 0x2539de1785b735999fb4dac35ee17ed0ef995d05ab2fc5faeaa69ae87bcec0a5,
68
+ 0x0c246c5a2ef8ee0126497f222b3e0a0ef4e1c3d41c86d46e43982cb11d77951d,
69
+ 0x192089c4974f68e95408148f7c0632edbb09e6a6ad1a1c2f3f0305f5d03b527b,
70
+ 0x1eae0ad8ab68b2f06a0ee36eeb0d0c058529097d91096b756d8fdc2fb5a60d85,
71
+ 0x179190e5d0e22179e46f8282872abc88db6e2fdc0dee99e69768bd98c5d06bfb,
72
+ 0x29bb9e2c9076732576e9a81c7ac4b83214528f7db00f31bf6cafe794a9b3cd1c,
73
+ 0x225d394e42207599403efd0c2464a90d52652645882aac35b10e590e6e691e08,
74
+ 0x064760623c25c8cf753d238055b444532be13557451c087de09efd454b23fd59,
75
+ 0x10ba3a0e01df92e87f301c4b716d8a394d67f4bf42a75c10922910a78f6b5b87,
76
+ 0x0e070bf53f8451b24f9c6e96b0c2a801cb511bc0c242eb9d361b77693f21471c,
77
+ 0x1b94cd61b051b04dd39755ff93821a73ccd6cb11d2491d8aa7f921014de252fb,
78
+ 0x1d7cb39bafb8c744e148787a2e70230f9d4e917d5713bb050487b5aa7d74070b,
79
+ 0x2ec93189bd1ab4f69117d0fe980c80ff8785c2961829f701bb74ac1f303b17db,
80
+ 0x2db366bfdd36d277a692bb825b86275beac404a19ae07a9082ea46bd83517926,
81
+ 0x062100eb485db06269655cf186a68532985275428450359adc99cec6960711b8,
82
+ 0x0761d33c66614aaa570e7f1e8244ca1120243f92fa59e4f900c567bf41f5a59b,
83
+ 0x20fc411a114d13992c2705aa034e3f315d78608a0f7de4ccf7a72e494855ad0d,
84
+ 0x25b5c004a4bdfcb5add9ec4e9ab219ba102c67e8b3effb5fc3a30f317250bc5a,
85
+ 0x23b1822d278ed632a494e58f6df6f5ed038b186d8474155ad87e7dff62b37f4b,
86
+ 0x22734b4c5c3f9493606c4ba9012499bf0f14d13bfcfcccaa16102a29cc2f69e0,
87
+ 0x26c0c8fe09eb30b7e27a74dc33492347e5bdff409aa3610254413d3fad795ce5,
88
+ 0x070dd0ccb6bd7bbae88eac03fa1fbb26196be3083a809829bbd626df348ccad9,
89
+ 0x12b6595bdb329b6fb043ba78bb28c3bec2c0a6de46d8c5ad6067c4ebfd4250da,
90
+ 0x248d97d7f76283d63bec30e7a5876c11c06fca9b275c671c5e33d95bb7e8d729,
91
+ 0x1a306d439d463b0816fc6fd64cc939318b45eb759ddde4aa106d15d9bd9baaaa,
92
+ 0x28a8f8372e3c38daced7c00421cb4621f4f1b54ddc27821b0d62d3d6ec7c56cf,
93
+ 0x0094975717f9a8a8bb35152f24d43294071ce320c829f388bc852183e1e2ce7e,
94
+ 0x04d5ee4c3aa78f7d80fde60d716480d3593f74d4f653ae83f4103246db2e8d65,
95
+ 0x2a6cf5e9aa03d4336349ad6fb8ed2269c7bef54b8822cc76d08495c12efde187,
96
+ 0x2304d31eaab960ba9274da43e19ddeb7f792180808fd6e43baae48d7efcba3f3,
97
+ 0x03fd9ac865a4b2a6d5e7009785817249bff08a7e0726fcb4e1c11d39d199f0b0,
98
+ 0x00b7258ded52bbda2248404d55ee5044798afc3a209193073f7954d4d63b0b64,
99
+ 0x159f81ada0771799ec38fca2d4bf65ebb13d3a74f3298db36272c5ca65e92d9a,
100
+ 0x1ef90e67437fbc8550237a75bc28e3bb9000130ea25f0c5471e144cf4264431f,
101
+ 0x1e65f838515e5ff0196b49aa41a2d2568df739bc176b08ec95a79ed82932e30d,
102
+ 0x2b1b045def3a166cec6ce768d079ba74b18c844e570e1f826575c1068c94c33f,
103
+ 0x0832e5753ceb0ff6402543b1109229c165dc2d73bef715e3f1c6e07c168bb173,
104
+ 0x02f614e9cedfb3dc6b762ae0a37d41bab1b841c2e8b6451bc5a8e3c390b6ad16,
105
+ 0x0e2427d38bd46a60dd640b8e362cad967370ebb777bedff40f6a0be27e7ed705,
106
+ 0x0493630b7c670b6deb7c84d414e7ce79049f0ec098c3c7c50768bbe29214a53a,
107
+ 0x22ead100e8e482674decdab17066c5a26bb1515355d5461a3dc06cc85327cea9,
108
+ 0x25b3e56e655b42cdaae2626ed2554d48583f1ae35626d04de5084e0b6d2a6f16,
109
+ 0x1e32752ada8836ef5837a6cde8ff13dbb599c336349e4c584b4fdc0a0cf6f9d0,
110
+ 0x2fa2a871c15a387cc50f68f6f3c3455b23c00995f05078f672a9864074d412e5,
111
+ 0x2f569b8a9a4424c9278e1db7311e889f54ccbf10661bab7fcd18e7c7a7d83505,
112
+ 0x044cb455110a8fdd531ade530234c518a7df93f7332ffd2144165374b246b43d,
113
+ 0x227808de93906d5d420246157f2e42b191fe8c90adfe118178ddc723a5319025,
114
+ 0x02fcca2934e046bc623adead873579865d03781ae090ad4a8579d2e7a6800355,
115
+ 0x0ef915f0ac120b876abccceb344a1d36bad3f3c5ab91a8ddcbec2e060d8befac,
116
+ ]
117
+
118
+ RC_FULL_END = [
119
+ [
120
+ 0x1797130f4b7a3e1777eb757bc6f287f6ab0fb85f6be63b09f3b16ef2b1405d38,
121
+ 0x0a76225dc04170ae3306c85abab59e608c7f497c20156d4d36c668555decc6e5,
122
+ 0x1fffb9ec1992d66ba1e77a7b93209af6f8fa76d48acb664796174b5326a31a5c,
123
+ 0x25721c4fc15a3f2853b57c338fa538d85f8fbba6c6b9c6090611889b797b9c5f,
124
+ ],
125
+ [
126
+ 0x0c817fd42d5f7a41215e3d07ba197216adb4c3790705da95eb63b982bfcaf75a,
127
+ 0x13abe3f5239915d39f7e13c2c24970b6df8cf86ce00a22002bc15866e52b5a96,
128
+ 0x2106feea546224ea12ef7f39987a46c85c1bc3dc29bdbd7a92cd60acb4d391ce,
129
+ 0x21ca859468a746b6aaa79474a37dab49f1ca5a28c748bc7157e1b3345bb0f959,
130
+ ],
131
+ [
132
+ 0x05ccd6255c1e6f0c5cf1f0df934194c62911d14d0321662a8f1a48999e34185b,
133
+ 0x0f0e34a64b70a626e464d846674c4c8816c4fb267fe44fe6ea28678cb09490a4,
134
+ 0x0558531a4e25470c6157794ca36d0e9647dbfcfe350d64838f5b1a8a2de0d4bf,
135
+ 0x09d3dca9173ed2faceea125157683d18924cadad3f655a60b72f5864961f1455,
136
+ ],
137
+ [
138
+ 0x0328cbd54e8c0913493f866ed03d218bf23f92d68aaec48617d4c722e5bd4335,
139
+ 0x2bf07216e2aff0a223a487b1a7094e07e79e7bcc9798c648ee3347dd5329d34b,
140
+ 0x1daf345a58006b736499c583cb76c316d6f78ed6a6dffc82111e11a63fe412df,
141
+ 0x176563472456aaa746b694c60e1823611ef39039b2edc7ff391e6f2293d2c404,
142
+ ],
143
+ ]
144
+
145
+
146
+ # ── Poseidon2 Permutation (t=4, d=5) ──
147
+
148
+ def _sbox(x: int) -> int:
149
+ """S-box: x^5 mod p."""
150
+ x2 = (x * x) % BN254_P
151
+ return (x2 * x2 % BN254_P) * x % BN254_P
152
+
153
+
154
+ def _matmul_external_4(state: List[int]) -> List[int]:
155
+ """External (MDS) matrix multiply for t=4."""
156
+ p = BN254_P
157
+ s = state
158
+
159
+ t_0 = (s[0] + s[1]) % p
160
+ t_1 = (s[2] + s[3]) % p
161
+ t_2 = (s[1] + s[1] + t_1) % p # 2*s[1] + t_1
162
+ t_3 = (s[3] + s[3] + t_0) % p # 2*s[3] + t_0
163
+ t_4 = (t_1 + t_1) % p
164
+ t_4 = (t_4 + t_4 + t_3) % p # 4*t_1 + t_3
165
+ t_5 = (t_0 + t_0) % p
166
+ t_5 = (t_5 + t_5 + t_2) % p # 4*t_0 + t_2
167
+ t_6 = (t_3 + t_5) % p
168
+ t_7 = (t_2 + t_4) % p
169
+
170
+ return [t_6, t_5, t_7, t_4]
171
+
172
+
173
+ def _matmul_internal_4(state: List[int]) -> List[int]:
174
+ """Internal matrix multiply for t=4: diag * x + sum(x)."""
175
+ p = BN254_P
176
+ s = state
177
+ total = (s[0] + s[1] + s[2] + s[3]) % p
178
+ return [
179
+ (MAT_DIAG4_M_1[i] * s[i] + total) % p
180
+ for i in range(4)
181
+ ]
182
+
183
+
184
+ def poseidon2_permute(state: List[int]) -> List[int]:
185
+ """Full Poseidon2 permutation for BN254, t=4."""
186
+ p = BN254_P
187
+ s = list(state)
188
+
189
+ # Initial external matrix
190
+ s = _matmul_external_4(s)
191
+
192
+ # 4 full beginning rounds
193
+ for r in range(4):
194
+ rc = RC_FULL_BEGIN[r]
195
+ s = [(s[i] + rc[i]) % p for i in range(4)]
196
+ s = [_sbox(x) for x in s]
197
+ s = _matmul_external_4(s)
198
+
199
+ # 56 partial rounds
200
+ for r in range(56):
201
+ s[0] = (s[0] + RC_PARTIAL[r]) % p
202
+ s[0] = _sbox(s[0])
203
+ s = _matmul_internal_4(s)
204
+
205
+ # 4 full ending rounds
206
+ for r in range(4):
207
+ rc = RC_FULL_END[r]
208
+ s = [(s[i] + rc[i]) % p for i in range(4)]
209
+ s = [_sbox(x) for x in s]
210
+ s = _matmul_external_4(s)
211
+
212
+ return s
213
+
214
+
215
+ # ── Poseidon2 Sponge (matching @zkpassport/poseidon2 FieldSponge) ──
216
+
217
+ class _FieldSponge:
218
+ """Poseidon2 sponge construction with rate=3, capacity=1, t=4."""
219
+
220
+ RATE = 3
221
+ T = 4
222
+ ABSORB = 0
223
+ SQUEEZE = 1
224
+
225
+ def __init__(self, domain_iv: int = 0):
226
+ self.state = [0, 0, 0, domain_iv]
227
+ self.cache = [0] * self.RATE
228
+ self.cache_size = 0
229
+ self.mode = self.ABSORB
230
+
231
+ def _perform_duplex(self) -> List[int]:
232
+ # Zero-pad the cache
233
+ for i in range(self.cache_size, self.RATE):
234
+ self.cache[i] = 0
235
+ # Add cache into sponge state
236
+ for i in range(self.RATE):
237
+ self.state[i] = (self.state[i] + self.cache[i]) % BN254_P
238
+ self.state = poseidon2_permute(self.state)
239
+ return self.state[:self.RATE]
240
+
241
+ def absorb(self, value: int) -> None:
242
+ if self.mode == self.ABSORB and self.cache_size == self.RATE:
243
+ self._perform_duplex()
244
+ self.cache[0] = value
245
+ self.cache_size = 1
246
+ elif self.mode == self.ABSORB and self.cache_size < self.RATE:
247
+ self.cache[self.cache_size] = value
248
+ self.cache_size += 1
249
+ elif self.mode == self.SQUEEZE:
250
+ self.cache[0] = value
251
+ self.cache_size = 1
252
+ self.mode = self.ABSORB
253
+
254
+ def squeeze(self) -> int:
255
+ if self.mode == self.SQUEEZE and self.cache_size == 0:
256
+ self.mode = self.ABSORB
257
+ self.cache_size = 0
258
+
259
+ if self.mode == self.ABSORB:
260
+ new_output = self._perform_duplex()
261
+ self.mode = self.SQUEEZE
262
+ for i in range(self.RATE):
263
+ self.cache[i] = new_output[i]
264
+ self.cache_size = self.RATE
265
+
266
+ result = self.cache[0]
267
+ for i in range(1, self.cache_size):
268
+ self.cache[i - 1] = self.cache[i]
269
+ self.cache_size -= 1
270
+ self.cache[self.cache_size] = 0
271
+ return result
272
+
273
+
274
+ def poseidon2_hash(inputs: List[int]) -> int:
275
+ """Hash a list of field elements using Poseidon2 sponge (fixed-length)."""
276
+ out_len = 1
277
+ iv = (len(inputs) << 64) + (out_len - 1)
278
+ sponge = _FieldSponge(iv)
279
+ for v in inputs:
280
+ sponge.absorb(v)
281
+ return sponge.squeeze()
282
+
283
+
284
+ # ── Byte Conversion ──
285
+
286
+ def _bytes_to_field_elements(data: bytes) -> List[int]:
287
+ """Convert bytes to BN254 field elements (31-byte chunks, big-endian)."""
288
+ if len(data) == 0:
289
+ return [0]
290
+
291
+ elements = []
292
+ chunk_size = 31
293
+ for i in range(0, len(data), chunk_size):
294
+ chunk = data[i:i + chunk_size]
295
+ value = int.from_bytes(chunk, byteorder="big")
296
+ elements.append(value % BN254_P)
297
+ return elements
298
+
299
+
300
+ # ── Core Hash Functions ──
301
+
302
+ def _poseidon2_bytes(data: bytes) -> str:
303
+ """Poseidon2 hash over arbitrary bytes, matching TS commitment.ts."""
304
+ elements = _bytes_to_field_elements(data)
305
+
306
+ state = 0
307
+ for i in range(0, len(elements), 2):
308
+ left = elements[i]
309
+ right = elements[i + 1] if i + 1 < len(elements) else 0
310
+ state = poseidon2_hash([(state + left) % BN254_P, right])
311
+
312
+ return "poseidon2:" + format(state, "064x")
313
+
314
+
315
+ def _sha256_bytes(data: bytes) -> str:
316
+ """SHA-256 hash over arbitrary bytes."""
317
+ h = hashlib.sha256(data).hexdigest()
318
+ return "sha256:" + h
319
+
320
+
321
+ def _poseidon2_pair(left: int, right: int) -> int:
322
+ """Poseidon2 hash of two field elements (for Merkle internal nodes)."""
323
+ return poseidon2_hash([left, right])
324
+
325
+
326
+ def _parse_hash_to_field(hash_str: str) -> int:
327
+ """Parse 'algorithm:hex' hash string to field element."""
328
+ colon = hash_str.index(":")
329
+ hex_str = hash_str[colon + 1:]
330
+ return int(hex_str, 16) % BN254_P
331
+
332
+
333
+ # ── Algorithm Resolution ──
334
+
335
+
336
+ def _resolve_algorithm() -> str:
337
+ """Resolve commitment algorithm from env var or default.
338
+ Default is 'sha256'. Poseidon2 is opt-in via PRIMUST_COMMITMENT_ALGORITHM=poseidon2
339
+ until an audited implementation (e.g. Barretenberg) is validated.
340
+ """
341
+ alg = os.environ.get("PRIMUST_COMMITMENT_ALGORITHM")
342
+ if alg == "poseidon2":
343
+ return "poseidon2"
344
+ return "sha256"
345
+
346
+
347
+ def _parse_hash_to_raw_bytes(hash_str: str) -> bytes:
348
+ """Parse 'algorithm:hex' hash string to raw bytes."""
349
+ colon = hash_str.index(":")
350
+ hex_str = hash_str[colon + 1:]
351
+ return bytes.fromhex(hex_str)
352
+
353
+
354
+ # ── Public API ──
355
+
356
+ CommitmentResult = Tuple[str, str] # (hash, algorithm)
357
+
358
+
359
+ def commit(data: bytes, algorithm: Optional[str] = None) -> CommitmentResult:
360
+ """
361
+ Compute a commitment hash over input bytes.
362
+
363
+ Args:
364
+ data: Raw content bytes (NEVER transmitted)
365
+ algorithm: 'sha256' (default) or 'poseidon2'. If None, uses
366
+ PRIMUST_COMMITMENT_ALGORITHM env var or defaults to 'sha256'.
367
+
368
+ Returns:
369
+ (hash_string, algorithm)
370
+ """
371
+ alg = algorithm if algorithm is not None else _resolve_algorithm()
372
+ if alg == "poseidon2":
373
+ return (_poseidon2_bytes(data), "poseidon2")
374
+ return (_sha256_bytes(data), "sha256")
375
+
376
+
377
+ def commit_output(data: bytes) -> CommitmentResult:
378
+ """Commitment for check output. Uses resolved algorithm."""
379
+ alg = _resolve_algorithm()
380
+ if alg == "poseidon2":
381
+ return (_poseidon2_bytes(data), "poseidon2")
382
+ return (_sha256_bytes(data), "sha256")
383
+
384
+
385
+ def build_commitment_root(
386
+ hashes: List[str], algorithm: Optional[str] = None
387
+ ) -> Optional[str]:
388
+ """
389
+ Build Merkle root over commitment hashes.
390
+ Uses resolved algorithm (SHA-256 default) for intermediate nodes.
391
+
392
+ Returns:
393
+ Merkle root string, or None for empty list.
394
+ Single hash returns unchanged.
395
+ """
396
+ alg = algorithm if algorithm is not None else _resolve_algorithm()
397
+ if len(hashes) == 0:
398
+ return None
399
+ if len(hashes) == 1:
400
+ return hashes[0]
401
+
402
+ if alg == "poseidon2":
403
+ return _build_poseidon2_merkle_root(hashes)
404
+ return _build_sha256_merkle_root(hashes)
405
+
406
+
407
+ def _build_poseidon2_merkle_root(hashes: List[str]) -> str:
408
+ layer = [_parse_hash_to_field(h) for h in hashes]
409
+
410
+ while len(layer) > 1:
411
+ next_layer = []
412
+ for i in range(0, len(layer), 2):
413
+ left = layer[i]
414
+ right = layer[i + 1] if i + 1 < len(layer) else layer[i]
415
+ next_layer.append(_poseidon2_pair(left, right))
416
+ layer = next_layer
417
+
418
+ return "poseidon2:" + format(layer[0], "064x")
419
+
420
+
421
+ def _build_sha256_merkle_root(hashes: List[str]) -> str:
422
+ layer = [_parse_hash_to_raw_bytes(h) for h in hashes]
423
+
424
+ while len(layer) > 1:
425
+ next_layer = []
426
+ for i in range(0, len(layer), 2):
427
+ left = layer[i]
428
+ right = layer[i + 1] if i + 1 < len(layer) else layer[i]
429
+ next_layer.append(hashlib.sha256(left + right).digest())
430
+ layer = next_layer
431
+
432
+ return "sha256:" + layer[0].hex()
433
+
434
+
435
+ def select_proof_level(stage_type: str) -> str:
436
+ """
437
+ Select the proof level for a given stage type.
438
+
439
+ deterministic_rule → mathematical (deterministic: same input + same policy = same output)
440
+ policy_engine → mathematical (OPA, Cedar, Drools — deterministic rule engines)
441
+ zkml_model → verifiable_inference
442
+ ml_model → execution
443
+ statistical_test → execution
444
+ custom_code → execution
445
+ witnessed → witnessed
446
+ """
447
+ mapping = {
448
+ "deterministic_rule": "mathematical",
449
+ "zkml_model": "verifiable_inference",
450
+ "ml_model": "execution",
451
+ "statistical_test": "execution",
452
+ "custom_code": "execution",
453
+ "witnessed": "witnessed",
454
+ "policy_engine": "mathematical",
455
+ }
456
+ result = mapping.get(stage_type)
457
+ if result is None:
458
+ raise ValueError(f"Unknown stage type: {stage_type}")
459
+ return result