Module adcp.reporting.canonical_json
canonical_json_utf8_v1() — the byte-exact reporting evidence encoding.
Every immutable reporting artifact (manifest, fingerprint, publication id) is
bound to bytes, not to a Python object.
Two producers in two languages must
agree on those bytes or the SHA-256 that binds them is worthless, so this
module is a deliberately restricted RFC 8785 / JCS profile rather than "JSON
with sort_keys=True":
- Object keys are ordered by raw UTF-16 code units — the ECMAScript / JCS ordering, never locale collation and (importantly) never Python's default code-point ordering. The two disagree exactly when a supplementary character (U+10000 and above, encoded as a surrogate pair starting at U+D800) is compared against a BMP character in U+E000..U+FFFF.
- Strings use ECMAScript
JSON.stringifyescaping with no Unicode normalization. Lone surrogates are escaped rather than emitted, matching well-formedJSON.stringify(ES2019). - Arrays retain order.
- Numbers are base-10 JSON safe integers only. Exact decimals belong in the wire as JSON strings; a float here is a bug, not a rounding question.
Noneisnull; there is no representation for "undefined", so callers drop absent fields instead of emitting them.- No insignificant whitespace anywhere.
The golden vectors in tests/test_reporting_canonical_json.py are the
cross-language contract.
They are copied byte-for-byte from the TypeScript
conformance suite; if you change anything in this module and those vectors
still pass, the change is safe.
Global variables
var MAX_SAFE_INTEGER-
ECMAScript
Number.MAX_SAFE_INTEGER. The contract permits only integers that survive a round trip through a JavaScript producer unchanged.
Functions
def canonical_json_sha256_v1(value: object) ‑> str-
Expand source code
def canonical_json_sha256_v1(value: object) -> str: """Lowercase hex SHA-256 of the canonical encoding of ``value``.""" return hashlib.sha256(canonical_json_utf8_v1(value)).hexdigest()Lowercase hex SHA-256 of the canonical encoding of
value. def canonical_json_utf8_v1(value: object) ‑> bytes-
Expand source code
def canonical_json_utf8_v1(value: object) -> bytes: """Encode ``value`` as ``canonical_json_utf8_v1`` bytes.""" return _encode(value).encode("utf-8")Encode
valueascanonical_json_utf8_v1()bytes. def reporting_fingerprint_v1(value: object) ‑> str-
Expand source code
def reporting_fingerprint_v1(value: object) -> str: """``sha256:<hex>`` fingerprint of the canonical encoding of ``value``. The ``sha256:`` prefix distinguishes a fingerprint (which binds a *logical structure*) from a bare content digest (which binds *exact bytes*) at every boundary that carries both. """ return f"sha256:{canonical_json_sha256_v1(value)}"sha256:<hex>fingerprint of the canonical encoding ofvalue.The
sha256:prefix distinguishes a fingerprint (which binds a logical structure) from a bare content digest (which binds exact bytes) at every boundary that carries both. def strip_absent(value: Any) ‑> Any-
Expand source code
def strip_absent(value: Any) -> Any: """Recursively drop ``None`` members so two encodings of one document agree. ``canonical_json_utf8_v1`` has no "undefined", so an absent optional field and a field explicitly set to ``null`` are different documents with different digests. Pydantic's ``exclude_none=True`` does this for models; this does it for the plain mappings that reach a fingerprint helper by another route, so both paths hash the same bytes. """ if isinstance(value, dict): return {key: strip_absent(item) for key, item in value.items() if item is not None} if isinstance(value, (list, tuple)): return [strip_absent(item) for item in value] return valueRecursively drop
Nonemembers so two encodings of one document agree.canonical_json_utf8_v1()has no "undefined", so an absent optional field and a field explicitly set tonullare different documents with different digests. Pydantic'sexclude_none=Truedoes this for models; this does it for the plain mappings that reach a fingerprint helper by another route, so both paths hash the same bytes.
Classes
class CanonicalJsonError (*args, **kwargs)-
Expand source code
class CanonicalJsonError(TypeError, ValueError): """A value cannot be represented in ``canonical_json_utf8_v1``. Deliberately both a :class:`TypeError` (it *is* a type problem) and a :class:`ValueError` (so a Pydantic validator that encodes a field surfaces it as a normal validation error rather than a 500). """A value cannot be represented in
canonical_json_utf8_v1().Deliberately both a :class:
TypeError(it is a type problem) and a :class:ValueError(so a Pydantic validator that encodes a field surfaces it as a normal validation error rather than a 500).Ancestors
- builtins.TypeError
- builtins.ValueError
- builtins.Exception
- builtins.BaseException