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.
- primust_artifact_core-1.0.0/.gitignore +22 -0
- primust_artifact_core-1.0.0/PKG-INFO +17 -0
- primust_artifact_core-1.0.0/pyproject.toml +33 -0
- primust_artifact_core-1.0.0/src/primust_artifact_core/__init__.py +31 -0
- primust_artifact_core-1.0.0/src/primust_artifact_core/canonical.py +84 -0
- primust_artifact_core-1.0.0/src/primust_artifact_core/commitment.py +459 -0
- primust_artifact_core-1.0.0/src/primust_artifact_core/signing.py +205 -0
- primust_artifact_core-1.0.0/src/primust_artifact_core/types/__init__.py +77 -0
- primust_artifact_core-1.0.0/src/primust_artifact_core/types/artifact.py +237 -0
- primust_artifact_core-1.0.0/src/primust_artifact_core/validate_artifact.py +218 -0
- primust_artifact_core-1.0.0/tests/__init__.py +0 -0
- primust_artifact_core-1.0.0/tests/test_canonical.py +87 -0
- primust_artifact_core-1.0.0/tests/test_commitment.py +137 -0
- primust_artifact_core-1.0.0/tests/test_signing.py +180 -0
- primust_artifact_core-1.0.0/tests/test_validate_artifact.py +313 -0
|
@@ -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
|