diff --git a/proof/region/v1/PROTOCOL.md b/proof/region/v1/PROTOCOL.md index 557314c0..b49a6f17 100644 --- a/proof/region/v1/PROTOCOL.md +++ b/proof/region/v1/PROTOCOL.md @@ -663,13 +663,16 @@ u64be(encoded_length) || claim identity || Arb receipt identity || MPFI receipt identity || первый semantic identity || второй semantic identity)`. Full-domain gate: `claim_spans_full_domain_v1` возвращает истину только когда -`domain_point_count` claim равен `2^24` (OUTPUT_CARDINALITY_V1); неканонический -claim возвращает typed rejection `foreign_input` вместо panic. Reduced-domain -proof несёт `full_domain=False` и никогда не авторизует family mint; -family mint дополнительно разрешает `domain_identity` и допускает отдельно -exact full manifest: единственный range `[0, 2^24)` и point count `2^24`. -Совпадение только point count или reduced-domain candidate этот gate не -проходят. +`domain_point_count` claim равен `2^24` (OUTPUT_CARDINALITY_V1) И его +`domain_identity` совпадает с content identity exact full manifest — +единственного range `[0, 2^24)` с point count `2^24`. +`exact_full_domain_manifest_v1` выводится из канонической кодировки протокола +и никогда не пинится отдельным hash; каноническая range-грамматика отвергает +overlap и adjacency, поэтому полного покрытия достигает ровно один manifest. +Неканонический claim возвращает typed rejection `foreign_input` вместо panic. +Reduced-domain proof несёт `full_domain=False` и никогда не авторизует family +mint. Совпадение только point count, только identity, чужая или мутированная +identity или reduced-domain candidate этот gate не проходят. ## Semantic verification и `SemanticVerificationReceiptV1` diff --git a/proof/region/v1/dual_proof.py b/proof/region/v1/dual_proof.py index 5c2ceac6..667883bc 100644 --- a/proof/region/v1/dual_proof.py +++ b/proof/region/v1/dual_proof.py @@ -230,9 +230,18 @@ def binds( def _claim_spans_full_domain_v1(claim: protocol.DualComparisonClaimV1) -> bool: - """Total predicate for an already canonical claim.""" + """Total predicate for an already canonical claim. - return claim.domain_point_count == protocol.OUTPUT_CARDINALITY_V1 + The mint gate binds the exact full manifest's content identity: a raw + claim keeps its domain identity as an unverified coordinate, so a bare + `2^24` point count never authorizes a family mint on its own. + """ + + return ( + claim.domain_point_count == protocol.OUTPUT_CARDINALITY_V1 + and claim.domain_identity + == protocol.exact_full_domain_manifest_v1().identity + ) def claim_spans_full_domain_v1(claim: object) -> bool | DualProofRejectedV1: diff --git a/proof/region/v1/region_proof_protocol.py b/proof/region/v1/region_proof_protocol.py index 55b4be5a..f84c44ff 100644 --- a/proof/region/v1/region_proof_protocol.py +++ b/proof/region/v1/region_proof_protocol.py @@ -7,7 +7,7 @@ from dataclasses import dataclass, field, fields from enum import IntEnum, StrEnum from fractions import Fraction -from functools import cached_property +from functools import cache, cached_property from itertools import pairwise from typing import Callable, Iterable, Iterator, NoReturn, TypeAlias @@ -543,6 +543,22 @@ def index_of(self, ordinal: int) -> int | None: return None +@cache +def exact_full_domain_manifest_v1() -> ReducedDomainManifestV1: + """The exact full manifest of the whole sRGB8 point space. + + The canonical range grammar rejects overlap and adjacency, so exactly one + manifest covers all `2^24` points: the single range `[0, 2^24)`. Its + content identity is the only domain identity the full-domain mint gate + admits; the value is derived from the canonical encoding, never pinned + as a standalone hash. + """ + + return ReducedDomainManifestV1( + ((0, OUTPUT_CARDINALITY_V1),), OUTPUT_CARDINALITY_V1 + ) + + class ComparatorKindV1(IntEnum): ARB = 1 MPFI = 2 diff --git a/proof/region/v1/tests/test_dual_proof.py b/proof/region/v1/tests/test_dual_proof.py index c5c68d40..838fc2e5 100644 --- a/proof/region/v1/tests/test_dual_proof.py +++ b/proof/region/v1/tests/test_dual_proof.py @@ -483,10 +483,11 @@ def test_reduced_domain_proof_does_not_permit_family_mint(self) -> None: self.assertFalse(dual_proof.claim_spans_full_domain_v1(candidate.claim)) claim = candidate.claim + full_identity = protocol.exact_full_domain_manifest_v1().identity full_claim = protocol.DualComparisonClaimV1( claim.job_identity, claim.definition_digest, - claim.domain_identity, + full_identity, claim.policy_identity, protocol.OUTPUT_CARDINALITY_V1, claim.comparator_identities, @@ -495,11 +496,25 @@ def test_reduced_domain_proof_does_not_permit_family_mint(self) -> None: claim.decision_digest, ) self.assertTrue(dual_proof.claim_spans_full_domain_v1(full_claim)) - one_short = protocol.DualComparisonClaimV1( + # A bare point count never authorizes the mint: the reduced-domain + # identity proves the domain is not the exact full manifest. + foreign_identity = protocol.DualComparisonClaimV1( claim.job_identity, claim.definition_digest, claim.domain_identity, claim.policy_identity, + protocol.OUTPUT_CARDINALITY_V1, + claim.comparator_identities, + claim.run_claim_identities, + claim.transcript_identities, + claim.decision_digest, + ) + self.assertFalse(dual_proof.claim_spans_full_domain_v1(foreign_identity)) + one_short = protocol.DualComparisonClaimV1( + claim.job_identity, + claim.definition_digest, + full_identity, + claim.policy_identity, protocol.OUTPUT_CARDINALITY_V1 - 1, claim.comparator_identities, claim.run_claim_identities, diff --git a/proof/region/v1/tests/test_full_domain_gate.py b/proof/region/v1/tests/test_full_domain_gate.py new file mode 100644 index 00000000..bd5ac587 --- /dev/null +++ b/proof/region/v1/tests/test_full_domain_gate.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Hostile contract for the full-domain mint gate. + +The family mint admits exactly one domain: the exact full manifest of the +whole sRGB8 point space, the single canonical range `[0, 2^24)` with +`point_count = 2^24`. Its content identity is the only domain identity a +full-domain claim may carry; a bare point count proves nothing, because a +parsed raw claim keeps the domain identity as an unverified coordinate. +""" + +from __future__ import annotations + +import hashlib +import sys +import unittest +from pathlib import Path + +PROOF = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(PROOF)) + +import dual_proof # noqa: E402 +import region_proof_protocol as protocol # noqa: E402 + + +def digest(label: int) -> bytes: + return hashlib.sha256(f"full-domain-gate-{label}".encode("ascii")).digest() + + +def make_claim( + *, domain_identity: bytes, point_count: int +) -> protocol.DualComparisonClaimV1: + return protocol.DualComparisonClaimV1( + digest(1), + digest(2), + domain_identity, + digest(3), + point_count, + (digest(4), digest(5)), + (digest(6), digest(7)), + (digest(8), digest(9)), + digest(10), + ) + + +class ExactFullDomainManifestTests(unittest.TestCase): + def test_manifest_is_the_single_canonical_range(self) -> None: + manifest = protocol.exact_full_domain_manifest_v1() + self.assertIs(type(manifest), protocol.ReducedDomainManifestV1) + self.assertEqual(manifest.ranges, ((0, protocol.OUTPUT_CARDINALITY_V1),)) + self.assertEqual(manifest.point_count, protocol.OUTPUT_CARDINALITY_V1) + + def test_manifest_reencode_is_byte_identical(self) -> None: + manifest = protocol.exact_full_domain_manifest_v1() + self.assertEqual( + protocol.ReducedDomainManifestV1.parse(manifest.encode()), manifest + ) + + def test_manifest_content_identity_is_stable(self) -> None: + first = protocol.exact_full_domain_manifest_v1().identity + second = protocol.exact_full_domain_manifest_v1().identity + self.assertEqual(len(first), 32) + self.assertEqual(first, second) + + def test_split_full_coverage_is_not_a_manifest(self) -> None: + # Two adjacent ranges covering the whole domain must fail canonical + # grammar, so the exact single range remains the only full manifest. + half = protocol.OUTPUT_CARDINALITY_V1 // 2 + with self.assertRaises(protocol.ProtocolErrorV1): + protocol.ReducedDomainManifestV1( + ((0, half), (half, protocol.OUTPUT_CARDINALITY_V1)), + protocol.OUTPUT_CARDINALITY_V1, + ) + + +class FullDomainGateTests(unittest.TestCase): + def test_exact_full_claim_spans_the_domain(self) -> None: + identity = protocol.exact_full_domain_manifest_v1().identity + claim = make_claim( + domain_identity=identity, point_count=protocol.OUTPUT_CARDINALITY_V1 + ) + self.assertTrue(dual_proof.claim_spans_full_domain_v1(claim)) + + def test_full_count_with_foreign_identity_does_not_span(self) -> None: + # Count alone cannot authorize a family mint: nothing verifies that a + # raw claim's domain identity belongs to the claimed point count. + claim = make_claim( + domain_identity=digest(42), point_count=protocol.OUTPUT_CARDINALITY_V1 + ) + self.assertFalse(dual_proof.claim_spans_full_domain_v1(claim)) + + def test_exact_identity_with_short_count_does_not_span(self) -> None: + identity = protocol.exact_full_domain_manifest_v1().identity + claim = make_claim( + domain_identity=identity, point_count=protocol.OUTPUT_CARDINALITY_V1 - 1 + ) + self.assertFalse(dual_proof.claim_spans_full_domain_v1(claim)) + + def test_mutated_identity_does_not_span(self) -> None: + mutated = bytearray(protocol.exact_full_domain_manifest_v1().identity) + mutated[0] ^= 0xFF + claim = make_claim( + domain_identity=bytes(mutated), + point_count=protocol.OUTPUT_CARDINALITY_V1, + ) + self.assertFalse(dual_proof.claim_spans_full_domain_v1(claim)) + + def test_foreign_input_is_typed_rejection(self) -> None: + rejection = dual_proof.claim_spans_full_domain_v1(object()) + self.assertIs(type(rejection), dual_proof.DualProofRejectedV1) + self.assertEqual( + rejection.reason, dual_proof.DualProofRejectionReasonV1.FOREIGN_INPUT + ) + + +if __name__ == "__main__": + unittest.main()