Skip to content

Commit 38694ae

Browse files
authored
Merge pull request #803 from morluto/agent/centralize-sha256-digest
Centralize SHA-256 digest ownership
2 parents 213575a + fc0152f commit 38694ae

7 files changed

Lines changed: 32 additions & 36 deletions

File tree

src/jacobian/canonical.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import hashlib
56
import json
67
import re
78
import unicodedata
@@ -17,6 +18,12 @@
1718
_DECIMAL_CHUNK_DIGITS = 9
1819

1920

21+
def sha256_digest(data: bytes) -> str:
22+
"""Return the canonical prefixed SHA-256 digest for immutable bytes."""
23+
24+
return "sha256:" + hashlib.sha256(data).hexdigest()
25+
26+
2027
class CanonicalizationError(ValueError):
2128
"""The input cannot be represented by Jacobian's canonical JSON profile."""
2229

src/jacobian/contracts/linear.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
from __future__ import annotations
44

5-
import hashlib
65
from typing import Annotated, Literal, Self
76

87
from pydantic import Field, StrictInt, StringConstraints, model_validator
98

10-
from jacobian.canonical import canonicalize_json
9+
from jacobian.canonical import canonicalize_json, sha256_digest
1110
from jacobian.contracts.capabilities import (
1211
CapabilityProviderAvailability,
1312
CapabilityProviderRuntime,
@@ -29,14 +28,10 @@
2928
]
3029

3130

32-
def _sha256(value: bytes) -> str:
33-
return f"sha256:{hashlib.sha256(value).hexdigest()}"
34-
35-
3631
def linear_variable_order_digest(variables: tuple[str, ...]) -> str:
3732
"""Bind the declared column order without inventing a generic object schema."""
3833

39-
return _sha256(canonicalize_json({"variables": list(variables)}))
34+
return sha256_digest(canonicalize_json({"variables": list(variables)}))
4035

4136

4237
def _require_bounded_rationals(values: tuple[CanonicalRational, ...]) -> None:

src/jacobian/contracts/sat.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import base64
66
import binascii
7-
import hashlib
87
import unicodedata
98
from collections.abc import Iterable, Sequence
109
from typing import Annotated, Literal, Self
@@ -18,7 +17,7 @@
1817
model_validator,
1918
)
2019

21-
from jacobian.canonical import canonicalize_json
20+
from jacobian.canonical import canonicalize_json, sha256_digest
2221
from jacobian.contracts.capabilities import (
2322
CapabilityProviderAvailability,
2423
CapabilityProviderRuntime,
@@ -52,10 +51,6 @@
5251
_PROOF_FORMAT_VERSION: Literal["drat-text/v1"] = "drat-text/v1"
5352

5453

55-
def _sha256(value: bytes) -> str:
56-
return f"sha256:{hashlib.sha256(value).hexdigest()}"
57-
58-
5954
class SatVariableBinding(ContractModel):
6055
"""One deterministic symbolic-name to DIMACS-ID binding."""
6156

@@ -126,7 +121,7 @@ def require_canonical_instance(self) -> Self:
126121
raise ValueError("clauses must be unique and canonically ordered")
127122
if self.variable_map_digest != sat_variable_map_digest(self.variables):
128123
raise ValueError("variable-map digest does not match the canonical map")
129-
if self.dimacs_digest != _sha256(self.to_dimacs_bytes()):
124+
if self.dimacs_digest != sha256_digest(self.to_dimacs_bytes()):
130125
raise ValueError(
131126
"DIMACS digest does not match the deterministic projection"
132127
)
@@ -378,7 +373,7 @@ def require_exact_raw_proof(self) -> Self:
378373
raw = _decode_base64(self.proof_base64)
379374
if self.proof_base64 != base64.b64encode(raw).decode("ascii"):
380375
raise ValueError("proof bytes must use canonical base64")
381-
if self.proof_digest != _sha256(raw):
376+
if self.proof_digest != sha256_digest(raw):
382377
raise ValueError("raw proof digest does not match the preserved bytes")
383378
_require_available_producer(self.producer)
384379
return self
@@ -397,7 +392,7 @@ def from_bytes(
397392
return cls(
398393
cnf=cnf,
399394
proof_base64=base64.b64encode(proof).decode("ascii"),
400-
proof_digest=_sha256(proof),
395+
proof_digest=sha256_digest(proof),
401396
producer=producer,
402397
resource_budget=resource_budget,
403398
)
@@ -473,7 +468,7 @@ def bind_exact_bytes(self) -> Self:
473468
raw = _decode_base64(self.proof_base64)
474469
if self.proof_base64 != base64.b64encode(raw).decode("ascii"):
475470
raise ValueError("LRAT proof must use canonical base64")
476-
if self.proof_digest != _sha256(raw) or self.proof_byte_count != len(raw):
471+
if self.proof_digest != sha256_digest(raw) or self.proof_byte_count != len(raw):
477472
raise ValueError("LRAT proof digest or byte count does not match")
478473
if len(raw) > self.limits.max_proof_bytes:
479474
raise ValueError("LRAT proof exceeds its declared byte limit")
@@ -490,7 +485,7 @@ def from_bytes(
490485
return cls(
491486
cnf=cnf,
492487
proof_base64=base64.b64encode(proof).decode("ascii"),
493-
proof_digest=_sha256(proof),
488+
proof_digest=sha256_digest(proof),
494489
proof_byte_count=len(proof),
495490
limits=limits,
496491
)
@@ -613,7 +608,7 @@ def canonicalize_cnf(
613608
variables=variables,
614609
clauses=clause_models,
615610
variable_map_digest=sat_variable_map_digest(variables),
616-
dimacs_digest=_sha256(_dimacs_bytes(len(variables), clause_models)),
611+
dimacs_digest=sha256_digest(_dimacs_bytes(len(variables), clause_models)),
617612
)
618613

619614

@@ -626,7 +621,7 @@ def sat_variable_map_digest(
626621
"variable_map_format": "jacobian.sat.variable-map/v1",
627622
"variables": [variable.model_dump(mode="json") for variable in variables],
628623
}
629-
return _sha256(canonicalize_json(payload))
624+
return sha256_digest(canonicalize_json(payload))
630625

631626

632627
def _literal_sort_key(literal: int) -> tuple[int, bool]:

src/jacobian/contracts/smt.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import base64
66
import binascii
7-
import hashlib
87
from typing import Annotated, Literal, Self
98

109
from pydantic import (
@@ -15,6 +14,7 @@
1514
model_validator,
1615
)
1716

17+
from jacobian.canonical import sha256_digest
1818
from jacobian.contracts.capabilities import (
1919
CapabilityInstallTier,
2020
CapabilityProviderAvailability,
@@ -54,10 +54,6 @@
5454
_ALETHE_HOLE_MARKER = b":rule hole"
5555

5656

57-
def _sha256(value: bytes) -> str:
58-
return f"sha256:{hashlib.sha256(value).hexdigest()}"
59-
60-
6157
def _decode_base64(value: str) -> bytes:
6258
try:
6359
return base64.b64decode(value, validate=True)
@@ -270,7 +266,7 @@ class SmtProblemArtifact(ContractModel):
270266
@model_validator(mode="after")
271267
def require_exact_profile_input(self) -> Self:
272268
_validate_single_query_profile(self.smtlib_text, self.logic)
273-
if self.smtlib_digest != _sha256(self.smtlib_text.encode("ascii")):
269+
if self.smtlib_digest != sha256_digest(self.smtlib_text.encode("ascii")):
274270
raise ValueError("SMT-LIB digest does not match the exact input bytes")
275271
return self
276272

@@ -284,7 +280,7 @@ def from_text(
284280
return cls(
285281
logic=logic,
286282
smtlib_text=smtlib_text,
287-
smtlib_digest=_sha256(smtlib_text.encode("ascii")),
283+
smtlib_digest=sha256_digest(smtlib_text.encode("ascii")),
288284
)
289285

290286
def raw_bytes(self) -> bytes:
@@ -325,7 +321,7 @@ def require_exact_raw_proof(self) -> Self:
325321
proof = _decode_base64(self.proof_base64)
326322
if self.proof_base64 != base64.b64encode(proof).decode("ascii"):
327323
raise ValueError("proof bytes must use canonical base64")
328-
if self.proof_digest != _sha256(proof):
324+
if self.proof_digest != sha256_digest(proof):
329325
raise ValueError("Alethe proof digest does not match the preserved bytes")
330326
holes = proof.count(_ALETHE_HOLE_MARKER)
331327
if self.alethe_hole_count != holes or self.contains_holes != (holes > 0):
@@ -359,7 +355,7 @@ def from_bytes(
359355
return cls(
360356
problem=problem,
361357
proof_base64=base64.b64encode(proof).decode("ascii"),
362-
proof_digest=_sha256(proof),
358+
proof_digest=sha256_digest(proof),
363359
alethe_hole_count=holes,
364360
contains_holes=holes > 0,
365361
producer=producer,

src/jacobian/storage/blobs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
from contextlib import contextmanager
1616
from pathlib import Path
1717

18+
from jacobian.canonical import sha256_digest
1819
from jacobian.storage.errors import (
1920
ArtifactIntegrityError,
2021
ArtifactNotFoundError,
2122
StorageError,
2223
StorageLimitError,
2324
)
24-
from jacobian.storage.identity import sha256_digest
2525

2626
_LOGGER = logging.getLogger(__name__)
2727

src/jacobian/storage/identity.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from dataclasses import dataclass
77
from typing import Final
88

9-
from jacobian.canonical import CanonicalLimits, canonicalize_json
9+
from jacobian.canonical import CanonicalLimits, canonicalize_json, sha256_digest
1010
from jacobian.contracts.artifacts import ArtifactManifest
1111
from jacobian.storage.errors import ArtifactNotFoundError
1212

@@ -35,10 +35,6 @@ class _ArtifactIdentity:
3535
manifest_bytes: bytes
3636

3737

38-
def sha256_digest(data: bytes) -> str:
39-
return "sha256:" + hashlib.sha256(data).hexdigest()
40-
41-
4238
def uri_from_digest(digest: str) -> str:
4339
return "artifact://sha256/" + digest.removeprefix("sha256:")
4440

@@ -143,6 +139,5 @@ def artifact_identity(
143139
"artifact_identity",
144140
"digest_from_uri",
145141
"framed_digest",
146-
"sha256_digest",
147142
"uri_from_digest",
148143
]

tests/unit/contracts/test_canonical_json.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import hashlib
4+
35
import pytest
46
from hypothesis import given, settings
57
from hypothesis import strategies as st
@@ -9,10 +11,16 @@
911
CanonicalizationError,
1012
CanonicalLimits,
1113
canonicalize_json,
14+
sha256_digest,
1215
)
1316
from jacobian.contracts.exact import CanonicalRational, require_bounded_rational
1417

1518

19+
def test_sha256_digest_uses_the_canonical_prefixed_format() -> None:
20+
for value in (b"", b"\x00", b"jacobian", b"\xde\xad\xbe\xef" * 100):
21+
assert sha256_digest(value) == "sha256:" + hashlib.sha256(value).hexdigest()
22+
23+
1624
def test_equivalent_rationals_have_identical_canonical_bytes() -> None:
1725
first = canonicalize_json({"weight": {"num": "2", "den": "4"}})
1826
second = canonicalize_json({"weight": {"num": "1", "den": "2"}})

0 commit comments

Comments
 (0)