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.stringify escaping with no Unicode normalization. Lone surrogates are escaped rather than emitted, matching well-formed JSON.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.
  • None is null; 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 value as canonical_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 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.

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 value

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.

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