From 452c7f8cde567fabb7b2a0bc273a01efe30703c4 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 7 Aug 2026 09:23:52 +0300 Subject: [PATCH 1/5] Proof: bind what each comparator coordinate contains, not only its digest (V5b2d-4i) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The manifest bound ten coordinate *numbers*. Nothing held the preimages those numbers fold. A domain separator inside the derivation could be rewritten, and two coordinates could be swapped, with the whole 408-test proof set staying green: measured on this tree, flipping "ordered admitted legal-file set; no legal-compliance claim" left the 267-test arb gate and the 408-test shared suite at their baseline, and swapping engine_release with upstream_source left all 58 test_pipeline contracts green. The meaning of a coordinate was therefore redefinable in silence, while comparator identity is the foundation of the decision chain. test_comparator_content.py binds the INPUT of the fold, in three independent layers over one deterministic derivation (synthetic source closure, mocked build backend, admitted local sources): - naming law: the coordinate named X must carry the domain separator that spells X, decoded by an independent wire-format oracle rather than by the pipeline encoder. Kills a swap of any two coordinates; - reference vector: exact preimage bytes of all eight source-bound coordinates, keyed by coordinate NAME. Kills any byte change, including separator edits and reorderings the structural layer cannot see. The covered set is derived from protocol.source_bound_coordinates_v2(), so a new source-bound coordinate cannot land without its own bytes; - structural golden: every preimage decoded to (separator, version, ordered chunks) and rendered without admitted source bytes — a file body prints as file:. Editing interval.c cannot move this golden, so any movement of it is semantic and regenerating it for a green CI is not a thing that happens. Layers do not duplicate the existing pins: file contents stay bound by _PINNED_BUILD_SOURCE_SHA256_V1, the build-process encoding stays bound by its own golden in test_build.py. The two BUILD-observation coordinates are outside the byte vector for that reason and are fully covered structurally. Both inventory pins updated from an executed gate: 267 -> 275 tests. --- proof/region/v1/arb/tests/gate.py | 2 +- .../v1/arb/tests/test_comparator_content.py | 505 ++++++++++++++++++ proof/region/v1/tests/test_build.py | 6 +- 3 files changed, 509 insertions(+), 4 deletions(-) create mode 100644 proof/region/v1/arb/tests/test_comparator_content.py diff --git a/proof/region/v1/arb/tests/gate.py b/proof/region/v1/arb/tests/gate.py index e4d97d4b..713d09ca 100644 --- a/proof/region/v1/arb/tests/gate.py +++ b/proof/region/v1/arb/tests/gate.py @@ -19,7 +19,7 @@ "test_mpfi_input.py", ) EXPECTED_TEST_INVENTORY_SHA256 = ( - "030cd7d43490c3aea5e10ba7d29baa2ab7de61639f05b9e9a98d0007cd990c05" + "fd8eabb5b1b2147d368552adb2d2637b33c72e1c8944c7c39d07121744e64abd" ) _EVALUATOR_REASON = "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary" EXPECTED_SKIPS = frozenset( diff --git a/proof/region/v1/arb/tests/test_comparator_content.py b/proof/region/v1/arb/tests/test_comparator_content.py new file mode 100644 index 00000000..e90b8a5d --- /dev/null +++ b/proof/region/v1/arb/tests/test_comparator_content.py @@ -0,0 +1,505 @@ +#!/usr/bin/env python3 +"""Закрепляет СОДЕРЖИМОЕ десяти координат Arb-компаратора. + +Манифест связывал только *числа* координат — sha256 от преимиджа. Сам преимидж +не удерживал никто: домен-сепаратор внутри деривации можно было переписать, а +две координаты — поменять местами, и весь набор оставался зелёным. Значение +координаты переопределялось молча, хотя идентичность компаратора — фундамент +решающей цепи. + +Модуль закрепляет ВХОД свёртки, а не её результат, тремя независимыми слоями: + +1. Закон именования: координата с именем ``X`` обязана нести домен-сепаратор, + который произносит ``X``. Ловит перестановку координат местами. +2. Reference-вектор: точные байты каждой source-bound координаты на + детерминированном входе, закреплённые по ИМЕНИ координаты. Ловит любое + изменение байтов, включая перестановку и правку сепаратора. +3. Структурный golden: разбор преимиджа на (сепаратор, версия, упорядоченные + чанки) в форме, не содержащей байтов допущенных исходников. Правка любого + сепаратора и любой сдвиг схемы дают читаемый diff; правка interval.c — + не даёт. Golden поэтому не подлежит регенерации «ради зелёного CI»: + его движение всегда семантическое. + +Байты допущенных исходников закреплены отдельно — ``_PINNED_BUILD_SOURCE_SHA256_V1`` +в ``arb/pipeline.py``. Слои не дублируют друг друга: там — содержимое файлов, +здесь — смысл координат. + +Две координаты BUILD-наблюдения (``build_identity``, ``test_observation``) +намеренно не входят в reference-вектор: их байты законно зависят от кодировки +процессов сборки, которая закреплена своим golden в ``tests/test_build.py``. +Структурный golden покрывает их полностью — процессы в нём непрозрачны. +""" + +from __future__ import annotations + +import hashlib +import sys +import unittest +from dataclasses import fields as dataclass_fields +from pathlib import Path + + +PROOF = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROOF)) + +# Детерминированный вход берётся из уже существующего фикстур-закрытия +# ``test_pipeline``, а не переписывается заново: второй экземпляр синтетических +# архивов расходился бы с первым и вектор перестал бы что-либо значить. +import test_pipeline # noqa: E402 +from arb import pipeline # noqa: E402 +import region_proof_protocol as protocol # noqa: E402 + + +COMPARATOR_LABEL_PREFIX_V1 = "labcolors.proof-region.arb-comparator." + +# Точные байты каждой source-bound координаты на детерминированном входе +# (синтетический source-закрытие из test_pipeline + допущенные локальные +# исходники). Ключ — ИМЯ координаты: перестановка двух координат местами +# меняет содержимое поля и краснит вектор. +SOURCE_BOUND_PREIMAGE_SHA256_V1 = { + "engine_release": "0b9557439aac7b93eceec78a93d44c00c77a565110fcfe8f90c139bed03b46f1", + "upstream_source": "7d0970948bbc52f91b1211ca985773eabb86007be0ce0d5cb02060e1979af29c", + "arithmetic_input_set": "006a093a27215c5fec14454ac6ec82f008519a437b98001d95d47689d703694d", + "wrapper_source": "957eb113f89a191ad0eade3cd2f9f3a9a1da021694921a670efa86949e680dc2", + "evaluator_source": "9e4f470d56a954a270f636c6c7befca26a77549f345cdaaa83cf35e097952db2", + "operation_allowlist": "bae12a908ba87c59c8126811739e85a472218f75c9325d47c2f72f2b7ddf2b79", + "legal_file_set": "5d9b4658d60406d014ddcc7f6ce47966fb795826ce0440d6d0186e283bcc43e1", + "exclusions": "3c9c249541dced3d1a35159244b4b6feebfa0d290587fdf0cd3c2c98f5941b41", +} + +# Содержимое каждой координаты в разобранном виде. `file:` вместо байтов +# допущенного файла — потому что содержимое файлов закреплено манифестом +# pipeline, а здесь закрепляется смысл, а не байты. Любое движение этого +# golden — семантическое. +COORDINATE_STRUCTURE_GOLDEN_V1 = """ +engine_release label=labcolors.proof-region.arb-comparator.engine-release.v1 version=1 chunks=3 + [00] "FLINT release lock declaration" + [01] bin + [02] bin:32 +upstream_source label=labcolors.proof-region.arb-comparator.upstream-source.v1 version=1 chunks=33 + [00] bin + [01] bin:32 + [02] bin:4 + [03] bin:1 + [04] bin + [05] bin:32 + [06] bin:32 + [07] bin:32 + [08] bin:8 + [09] bin:8 + [10] bin + [11] bin:8 + [12] bin:32 + [13] bin:1 + [14] bin + [15] bin:32 + [16] bin:32 + [17] bin:32 + [18] bin:8 + [19] bin:8 + [20] bin + [21] bin:8 + [22] bin:32 + [23] bin:1 + [24] bin + [25] bin:32 + [26] bin:32 + [27] bin:32 + [28] bin:8 + [29] bin:8 + [30] bin + [31] bin:8 + [32] bin:32 +arithmetic_input_set label=labcolors.proof-region.arb-comparator.arithmetic-input-set.v1 version=1 chunks=18 + [00] "exact admitted GMP MPFR FLINT source snapshots and pinned static-build boundary" + [01] bin:4 + [02] bin:1 + [03] bin:32 + [04] bin:32 + [05] bin:32 + [06] bin:1 + [07] bin:32 + [08] bin:32 + [09] bin:32 + [10] bin:1 + [11] bin:32 + [12] bin:32 + [13] bin:32 + [14] "gcc@sha256:c74b2d34b775e6a1b14b13b1d41dc7233f62a18f7a6a4e139e0cf59eeab2e070" + [15] "linux/amd64" + [16] bin:32 + [17] bin:32 +wrapper_source label=labcolors.proof-region.arb-comparator.wrapper-source.v1 version=1 chunks=13 + [00] bin:4 + [01] "proof/region/v1/arb/evaluator/formula.h" + [02] bin:4 + [03] bin:8 + [04] file:proof/region/v1/arb/evaluator/formula.h + [05] "proof/region/v1/arb/evaluator/interval.c" + [06] bin:4 + [07] bin:8 + [08] file:proof/region/v1/arb/evaluator/interval.c + [09] "proof/region/v1/arb/evaluator/interval.h" + [10] bin:4 + [11] bin:8 + [12] file:proof/region/v1/arb/evaluator/interval.h +evaluator_source label=labcolors.proof-region.arb-comparator.evaluator-source.v1 version=1 chunks=33 + [00] bin:4 + [01] "generated/formula.generated.c" + [02] bin:4 + [03] bin:8 + [04] file:generated/formula.generated.c + [05] "proof/region/v1/arb/evaluator/hash.c" + [06] bin:4 + [07] bin:8 + [08] file:proof/region/v1/arb/evaluator/hash.c + [09] "proof/region/v1/arb/evaluator/hash.h" + [10] bin:4 + [11] bin:8 + [12] file:proof/region/v1/arb/evaluator/hash.h + [13] "proof/region/v1/arb/evaluator/main.c" + [14] bin:4 + [15] bin:8 + [16] file:proof/region/v1/arb/evaluator/main.c + [17] "proof/region/v1/arb/evaluator/region.c" + [18] bin:4 + [19] bin:8 + [20] file:proof/region/v1/arb/evaluator/region.c + [21] "proof/region/v1/arb/evaluator/region.h" + [22] bin:4 + [23] bin:8 + [24] file:proof/region/v1/arb/evaluator/region.h + [25] "proof/region/v1/arb/evaluator/wire.c" + [26] bin:4 + [27] bin:8 + [28] file:proof/region/v1/arb/evaluator/wire.c + [29] "proof/region/v1/arb/evaluator/wire.h" + [30] bin:4 + [31] bin:8 + [32] file:proof/region/v1/arb/evaluator/wire.h +build_identity label=labcolors.proof-region.arb-comparator.build-identity.v2 version=2 chunks=14 + [00] file:proof/region/v1/arb/build.sh + [01] bin:32 + [02] bin:32 + [03] bin:32 + [04] bin:32 + [05] "build-observation=diagnostic-unsealed-v1" + [06] bin:4 + [07] bin + [08] bin + [09] bin:32 + [10] bin:32 + [11] bin:32 + [12] bin:8 + [13] bin:32 +operation_allowlist label=labcolors.proof-region.arb-comparator.operation-allowlist.v1 version=1 chunks=22 + [00] "exact-real-ssa-operator-declarations" + [01] bin:4 + [02] "operator lookup 2 real table_u8_exact_dyadic_at_ordinal" + [03] "operator eq 2 bool exact_same_type_equality" + [04] "operator select 3 same bool_true_second_else_third" + [05] "operator add 2 real exact_x_plus_y" + [06] "operator sub 2 real exact_x_minus_y" + [07] "operator mul 2 real exact_x_times_y" + [08] "operator div 2 real domain_y_ne_zero_x_div_y_else_domain_unproven" + [09] "operator min 2 real exact_lesser_real" + [10] "operator max 2 real exact_greater_real" + [11] "operator root3 1 real domain_x_ge_zero_unique_y_ge_zero_y_cubed_eq_x_else_domain_unproven" + [12] "operator sqrt 1 real domain_x_ge_zero_unique_y_ge_zero_y_squared_eq_x_else_domain_unproven" + [13] "operator exp 1 real analytic_natural_exponential" + [14] "operator log 1 real domain_x_gt_zero_analytic_natural_logarithm_else_domain_unproven" + [15] "operator sin 1 real analytic_sine_radians" + [16] "operator cos 1 real analytic_cosine_radians" + [17] "operator abs 1 real exact_absolute_value" + [18] "operator sign 1 real negative_minus_one_zero_zero_positive_one" + [19] "operator pow_pos 2 real domain_x_gt_zero_exp_y_mul_log_x_else_domain_unproven" + [20] "operator pow_nn 2 real if_x_eq_zero_and_y_gt_zero_zero_else_pow_pos" + [21] "operator ratio0 2 real if_x_eq_zero_and_y_eq_zero_zero_else_domain_y_gt_zero_x_div_y" +test_observation label=labcolors.proof-region.arb-comparator.test-observation.v1 version=1 chunks=5 + [00] "kind:aggregate-outer-process-observation-no-per-test-records" + [01] file:proof/region/v1/arb/build.sh + [02] bin:4 + [03] bin + [04] bin +legal_file_set label=labcolors.proof-region.arb-comparator.legal-file-set.v1 version=1 chunks=32 + [00] "ordered admitted legal-file set; no legal-compliance claim" + [01] bin:4 + [02] bin:1 + [03] bin:32 + [04] bin:32 + [05] bin:32 + [06] bin:4 + [07] bin:51 + [08] "LICENSE" + [09] bin:4 + [10] bin:8 + [11] bin:32 + [12] bin:1 + [13] bin:32 + [14] bin:32 + [15] bin:32 + [16] bin:4 + [17] bin:51 + [18] "LICENSE" + [19] bin:4 + [20] bin:8 + [21] bin:32 + [22] bin:1 + [23] bin:32 + [24] bin:32 + [25] bin:32 + [26] bin:4 + [27] bin:51 + [28] "LICENSE" + [29] bin:4 + [30] bin:8 + [31] bin:32 +exclusions label=labcolors.proof-region.arb-comparator.exclusions.v1 version=1 chunks=12 + [00] "gap:host-and-docker-daemon-not-source-bound" + [01] "gap:unsealed-diagnostic-build-observer" + [02] "gap:libc-libm-libpthread-libgcc-and-build-utility-source" + [03] "gap:no-per-test-result-records" + [04] "gap:no-git-derivation-for-project-pinned-release-only-files" + [05] "gap:no-origin-authority-reverification" + [06] "unsealed-linux-x64-docker-host" + [07] "build-observation=diagnostic-unsealed-v1" + [08] bin:4 + [09] "ci/omitted" + [10] bin:4 + [11] bin:57 +""".strip() + + +class PreimageDecodeErrorV1(ValueError): + """Разбор не восстановил ровно исходные байты преимиджа.""" + + +def decode_comparator_preimage_v1( + preimage: bytes, +) -> tuple[bytes, int, tuple[bytes, ...]]: + """Независимый тотальный разбор преимиджа компаратора. + + Оракул написан от формата провода, а не вызовом кодировщика pipeline: + закон именования и golden иначе доказывали бы сами себя. Разбор обязан + потребить ровно все байты — иначе схема сдвинулась и утверждать по ней + нельзя. + """ + + if type(preimage) is not bytes or not preimage: + raise PreimageDecodeErrorV1("preimage must be nonempty bytes") + separator = preimage.find(b"\0") + if separator < 1: + raise PreimageDecodeErrorV1("preimage has no domain separator") + label = preimage[: separator + 1] + header = separator + 1 + if len(preimage) < header + 5: + raise PreimageDecodeErrorV1("preimage is truncated before its chunk count") + version = preimage[header] + count = int.from_bytes(preimage[header + 1 : header + 5], "big") + offset = header + 5 + chunks: list[bytes] = [] + for _ in range(count): + if len(preimage) < offset + 8: + raise PreimageDecodeErrorV1("preimage is truncated inside a chunk length") + length = int.from_bytes(preimage[offset : offset + 8], "big") + offset += 8 + if len(preimage) < offset + length: + raise PreimageDecodeErrorV1("preimage is truncated inside a chunk body") + chunks.append(preimage[offset : offset + length]) + offset += length + if offset != len(preimage): + raise PreimageDecodeErrorV1("preimage has trailing bytes after its chunks") + return label, version, tuple(chunks) + + +def _render_chunk_v1(chunk: bytes, admitted_by_content: dict[bytes, str]) -> str: + """Рендер одного чанка без байтов допущенного исходника.""" + + path = admitted_by_content.get(chunk) + if path is not None: + return f"file:{path}" + printable = chunk and all(0x20 <= byte <= 0x7E and byte != 0x22 for byte in chunk) + if printable and len(chunk) <= 128: + return f'"{chunk.decode("ascii")}"' + # Длину печатаем только для коротких чанков: они структурные (счётчики, + # дайджесты, роли). Длинные — это содержимое, и его размер здесь не + # закрепляется, иначе golden двигался бы от правки исходника. + return f"bin:{len(chunk)}" if len(chunk) <= 64 else "bin" + + +def render_coordinate_structure_v1( + preimages: pipeline.ArbComparatorPreimagesV1, + admitted_by_content: dict[bytes, str], +) -> str: + lines: list[str] = [] + for field in dataclass_fields(preimages): + label, version, chunks = decode_comparator_preimage_v1( + getattr(preimages, field.name) + ) + lines.append( + f"{field.name} label={label[:-1].decode('ascii')} " + f"version={version} chunks={len(chunks)}" + ) + lines.extend( + f" [{index:02d}] {_render_chunk_v1(chunk, admitted_by_content)}" + for index, chunk in enumerate(chunks) + ) + return "\n".join(lines) + + +def _derived_comparator_v1() -> pipeline.DiagnosticArbComparatorV1: + binary = test_pipeline._static_elf(b"derived-comparator") + result = pipeline.ControlledPipelineV1( + build_backend=test_pipeline._BuildBackend((binary, binary)), + ).build(test_pipeline._request()) + if type(result) is not pipeline.DiagnosticBuildObservationV1: + raise AssertionError(result) + return result.comparator + + +def _admitted_by_content_v1() -> dict[bytes, str]: + files = test_pipeline._build_sources().files + by_content = {item.contents: item.path for item in files} + if len(by_content) != len(files): + raise AssertionError("admitted build sources are not content-distinct") + return by_content + + +class PreimageDecoderTests(unittest.TestCase): + """Оракул обязан быть чувствителен сам по себе.""" + + def test_decoder_recovers_exact_parts_of_a_hand_built_preimage(self) -> None: + preimage = ( + b"labcolors.proof-region.arb-comparator.example.v1\0" + + b"\x01" + + (2).to_bytes(4, "big") + + (5).to_bytes(8, "big") + + b"alpha" + + (0).to_bytes(8, "big") + ) + + label, version, chunks = decode_comparator_preimage_v1(preimage) + + self.assertEqual(label, b"labcolors.proof-region.arb-comparator.example.v1\0") + self.assertEqual(version, 1) + self.assertEqual(chunks, (b"alpha", b"")) + + def test_decoder_rejects_truncation_trailing_bytes_and_a_missing_separator( + self, + ) -> None: + valid = ( + b"labcolors.proof-region.arb-comparator.example.v1\0" + + b"\x01" + + (1).to_bytes(4, "big") + + (5).to_bytes(8, "big") + + b"alpha" + ) + mutants = ( + valid[:-1], + valid + b"\x00", + valid.replace(b"\0", b"!"), + b"", + valid[: valid.index(b"\0") + 3], + ) + + for index, mutant in enumerate(mutants): + with self.subTest(mutant=index): + with self.assertRaises(PreimageDecodeErrorV1): + decode_comparator_preimage_v1(mutant) + + def test_decoder_reproduces_every_derived_coordinate_byte_for_byte(self) -> None: + preimages = _derived_comparator_v1().preimages + + for field in dataclass_fields(preimages): + with self.subTest(coordinate=field.name): + preimage = getattr(preimages, field.name) + label, version, chunks = decode_comparator_preimage_v1(preimage) + replayed = ( + label + + bytes((version,)) + + len(chunks).to_bytes(4, "big") + + b"".join( + len(chunk).to_bytes(8, "big") + chunk for chunk in chunks + ) + ) + self.assertEqual(replayed, preimage) + + +class CoordinateNamingLawTests(unittest.TestCase): + def test_every_coordinate_carries_the_domain_separator_that_names_it(self) -> None: + preimages = _derived_comparator_v1().preimages + labels: list[bytes] = [] + + for field in dataclass_fields(preimages): + with self.subTest(coordinate=field.name): + label, version, _chunks = decode_comparator_preimage_v1( + getattr(preimages, field.name) + ) + labels.append(label) + mnemonic = field.name.replace("_", "-") + self.assertEqual( + label.decode("ascii"), + f"{COMPARATOR_LABEL_PREFIX_V1}{mnemonic}.v{version}\0", + ) + + self.assertEqual(len(set(labels)), len(labels)) + self.assertEqual(len(labels), 10) + + +class SourceBoundReferenceVectorTests(unittest.TestCase): + def test_the_vector_covers_exactly_the_source_bound_coordinate_set(self) -> None: + # Вектор выводится из протокола, а не переписывается рукой: новая + # source-bound координата обязана получить свои байты, а не проехать + # мимо закрепления. + self.assertEqual( + tuple(SOURCE_BOUND_PREIMAGE_SHA256_V1), + protocol.source_bound_coordinates_v2(), + ) + self.assertEqual(len(SOURCE_BOUND_PREIMAGE_SHA256_V1), 8) + + def test_source_bound_coordinates_keep_their_exact_reference_preimages( + self, + ) -> None: + preimages = _derived_comparator_v1().preimages + + for name, expected in SOURCE_BOUND_PREIMAGE_SHA256_V1.items(): + with self.subTest(coordinate=name): + self.assertEqual( + hashlib.sha256(getattr(preimages, name)).hexdigest(), + expected, + ) + + +class CoordinateStructureGoldenTests(unittest.TestCase): + def test_coordinate_structure_matches_the_content_free_golden(self) -> None: + comparator = _derived_comparator_v1() + + self.assertEqual( + render_coordinate_structure_v1( + comparator.preimages, + _admitted_by_content_v1(), + ), + COORDINATE_STRUCTURE_GOLDEN_V1, + ) + + def test_the_golden_holds_no_admitted_source_bytes(self) -> None: + # Доказывает разделение слоёв: правка допущенного файла не двигает + # этот golden, поэтому любое его движение — семантическое, и + # регенерация «ради зелёного CI» невозможна. + admitted = _admitted_by_content_v1() + rendered = render_coordinate_structure_v1( + _derived_comparator_v1().preimages, + admitted, + ).encode("ascii") + + for contents, path in admitted.items(): + with self.subTest(path=path): + self.assertEqual( + _render_chunk_v1(contents, admitted), + f"file:{path}", + ) + self.assertNotIn(contents, rendered) + + +if __name__ == "__main__": + unittest.main() diff --git a/proof/region/v1/tests/test_build.py b/proof/region/v1/tests/test_build.py index 3c1ac84d..4fc2d62b 100644 --- a/proof/region/v1/tests/test_build.py +++ b/proof/region/v1/tests/test_build.py @@ -54,12 +54,12 @@ def _temporary_mode(path: Path, mode: int) -> Iterator[None]: # Keep an independent outer oracle: importing the gate's expected hash here # would let a coordinated gate edit hide inventory drift. ARB_INVENTORY_SHA256_V1 = ( - "030cd7d43490c3aea5e10ba7d29baa2ab7de61639f05b9e9a98d0007cd990c05" + "fd8eabb5b1b2147d368552adb2d2637b33c72e1c8944c7c39d07121744e64abd" ) ARB_ORDER_SHA256_V1 = ( - "d7210149257cb51bd3df3397f8a69323977db8b83425ee28baa42d441685bcbf" + "48c29adda88a367aaa98061271bcee333139a891cb8b600dfd3baae97699d628" ) -ARB_TEST_COUNT_V1 = 267 +ARB_TEST_COUNT_V1 = 275 MOVED_INPUT_SURFACE_V1 = ( "CanonicalInputLimitsV1", From 8392c5287dc0cedaef49404a4f5c5ebafb65b1c2 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 7 Aug 2026 11:10:32 +0300 Subject: [PATCH 2/5] Review fixes: honest layer boundaries and compact failure output - Docstring no longer overclaims: the swap class was open for the engine_release/upstream_source pair (wrapper/evaluator was already caught by a pre-existing test); the structural golden pins only chunk order and count for BUILD coordinates and only presence/position for chunks over 64 bytes. - Failure paths no longer dump megabyte objects: the derived-comparator guard raises type name + repr sha256, and the decoder replay test compares preimage hashes and names the coordinate. --- .../v1/arb/tests/test_comparator_content.py | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/proof/region/v1/arb/tests/test_comparator_content.py b/proof/region/v1/arb/tests/test_comparator_content.py index e90b8a5d..8952fba5 100644 --- a/proof/region/v1/arb/tests/test_comparator_content.py +++ b/proof/region/v1/arb/tests/test_comparator_content.py @@ -3,9 +3,9 @@ Манифест связывал только *числа* координат — sha256 от преимиджа. Сам преимидж не удерживал никто: домен-сепаратор внутри деривации можно было переписать, а -две координаты — поменять местами, и весь набор оставался зелёным. Значение -координаты переопределялось молча, хотя идентичность компаратора — фундамент -решающей цепи. +пару ``engine_release``/``upstream_source`` — поменять местами, и весь набор +оставался зелёным. Значение координаты переопределялось молча, хотя +идентичность компаратора — фундамент решающей цепи. Модуль закрепляет ВХОД свёртки, а не её результат, тремя независимыми слоями: @@ -16,9 +16,11 @@ изменение байтов, включая перестановку и правку сепаратора. 3. Структурный golden: разбор преимиджа на (сепаратор, версия, упорядоченные чанки) в форме, не содержащей байтов допущенных исходников. Правка любого - сепаратора и любой сдвиг схемы дают читаемый diff; правка interval.c — - не даёт. Golden поэтому не подлежит регенерации «ради зелёного CI»: - его движение всегда семантическое. + сепаратора, числа или порядка чанков и длины короткого (≤64 Б) чанка дают + читаемый diff; чанк длиннее 64 Б закреплён только присутствием и позицией, + его байты и длина этим слоем не видны. Правка interval.c diff не даёт. + Golden поэтому не подлежит регенерации «ради зелёного CI»: его движение + всегда семантическое. Байты допущенных исходников закреплены отдельно — ``_PINNED_BUILD_SOURCE_SHA256_V1`` в ``arb/pipeline.py``. Слои не дублируют друг друга: там — содержимое файлов, @@ -26,8 +28,11 @@ Две координаты BUILD-наблюдения (``build_identity``, ``test_observation``) намеренно не входят в reference-вектор: их байты законно зависят от кодировки -процессов сборки, которая закреплена своим golden в ``tests/test_build.py``. -Структурный golden покрывает их полностью — процессы в нём непрозрачны. +процессов сборки. Структурный golden покрывает только порядок и число их +чанков; байты процессов — вне этого слоя, их закрепляет golden в +``tests/test_build.py``. Перестановка stdout/stderr внутри +``build_process_bytes_v1`` этим модулем не ловится — это известная граница +слоя, не покрытие. """ from __future__ import annotations @@ -353,7 +358,10 @@ def _derived_comparator_v1() -> pipeline.DiagnosticArbComparatorV1: build_backend=test_pipeline._BuildBackend((binary, binary)), ).build(test_pipeline._request()) if type(result) is not pipeline.DiagnosticBuildObservationV1: - raise AssertionError(result) + # Не печатаем сам объект: наблюдение сборки несёт мегабайтные байты + # процессов и преимиджей. Тип + sha256 от repr идентифицируют отказ. + digest = hashlib.sha256(repr(result).encode("utf-8", "replace")).hexdigest() + raise AssertionError(f"{type(result).__name__} repr-sha256:{digest}") return result.comparator @@ -422,7 +430,14 @@ def test_decoder_reproduces_every_derived_coordinate_byte_for_byte(self) -> None len(chunk).to_bytes(8, "big") + chunk for chunk in chunks ) ) - self.assertEqual(replayed, preimage) + # Сравниваем хеши, а не байты: при расхождении assertEqual + # печатал бы 144-КБ преимиджи целиком. Имя координаты — в + # сообщении, чтобы отказ оставался адресным. + self.assertEqual( + hashlib.sha256(replayed).hexdigest(), + hashlib.sha256(preimage).hexdigest(), + field.name, + ) class CoordinateNamingLawTests(unittest.TestCase): From 772edf33f01b130066bd1e846bd410f6fa142e40 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 7 Aug 2026 15:51:59 +0300 Subject: [PATCH 3/5] Proof: name the two-place repin the reference vector actually requires MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docstring claimed the layers do not overlap — pinned source bytes there, coordinate meaning here. A mutation falsified it: editing `interval.c` and correctly repinning `_PINNED_BUILD_SOURCE_SHA256_V1` still reddens the `wrapper_source` reference vector, because the vector folds the admitted files transitively. Whoever edits an evaluator source needs both repins, and the docstring now says so. The structural golden stays green through that edit, which is the point of its coarseness: it cannot be regenerated into agreement. --- proof/region/v1/arb/tests/test_comparator_content.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/proof/region/v1/arb/tests/test_comparator_content.py b/proof/region/v1/arb/tests/test_comparator_content.py index 8952fba5..e0ea968c 100644 --- a/proof/region/v1/arb/tests/test_comparator_content.py +++ b/proof/region/v1/arb/tests/test_comparator_content.py @@ -23,8 +23,13 @@ всегда семантическое. Байты допущенных исходников закреплены отдельно — ``_PINNED_BUILD_SOURCE_SHA256_V1`` -в ``arb/pipeline.py``. Слои не дублируют друг друга: там — содержимое файлов, -здесь — смысл координат. +в ``arb/pipeline.py``. Слои пересекаются, и это надо знать перед правкой: +reference-вектор сворачивает содержимое допущенных файлов транзитивно, поэтому +правка любого ``arb/evaluator/*.c|*.h`` требует репина в ДВУХ местах — в +``_PINNED_BUILD_SOURCE_SHA256_V1`` и в ``SOURCE_BOUND_PREIMAGE_SHA256_V1`` +(координата ``wrapper_source``). Обновление только первого оставляет вектор +красным. Структурный golden при этом зелёный: он не видит байты чанков длиннее +64 Б, и именно поэтому его нельзя «перегенерировать ради зелёного CI». Две координаты BUILD-наблюдения (``build_identity``, ``test_observation``) намеренно не входят в reference-вектор: их байты законно зависят от кодировки From c30a885838f5164ad452ef7f4b64076c3242495f Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:02:11 +0300 Subject: [PATCH 4/5] Proof: pin the reference vector to a zlib-independent fixture archive (V5b2d-4i) test_pipeline._tar() synthesized the admitted source archives with gzip.compress(compresslevel=9). Deflate output is not stable across zlib versions: Python 3.12.3 (zlib 1.2.13) and the pinned CI image python:3.14.6-slim (zlib 1.3.1) emit different bytes for identical input, which moved archive_sha256 -> source_lock_identity -> the four archive-derived source-bound preimages (engine_release, upstream_source, arithmetic_input_set, legal_file_set). The raw ustar stream was proven identical across versions, so only the container drifted. The fixture now encodes archives as a canonical RFC 1951 stored-deflate gzip member whose bytes are a pure function of the raw tar stream and cannot vary with the interpreter's zlib. A regression test (FixtureArchiveDeterminismTests) rebuilds the canonical member independently and reddens on a return to gzip.compress. Pins recomputed by executing the gates in the pinned container: SOURCE_BOUND_PREIMAGE_SHA256_V1 (4 archive-derived coordinates), arb gate inventory (293 tests) in gate.py and test_build.py, and the build-process characterization pins in test_build.py (input_bundle_identity, process_bytes sha256, comparator/evidence/claim identities). --- proof/region/v1/arb/tests/gate.py | 2 +- .../v1/arb/tests/test_comparator_content.py | 59 +++++++++++++++++-- proof/region/v1/arb/tests/test_pipeline.py | 32 +++++++++- proof/region/v1/tests/test_build.py | 24 ++++---- 4 files changed, 98 insertions(+), 19 deletions(-) diff --git a/proof/region/v1/arb/tests/gate.py b/proof/region/v1/arb/tests/gate.py index 76413632..d0ae7c11 100644 --- a/proof/region/v1/arb/tests/gate.py +++ b/proof/region/v1/arb/tests/gate.py @@ -19,7 +19,7 @@ "test_mpfi_input.py", ) EXPECTED_TEST_INVENTORY_SHA256 = ( - "3976521dfa61bb3be30afdaab0ed3066a81ea8c633ba7de413ef07962ed2ab0b" + "e61831a344284a253aa5e292eee97ba9ef0f17f512edd4d868a496c0562818d8" ) _EVALUATOR_REASON = "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary" EXPECTED_SKIPS = frozenset( diff --git a/proof/region/v1/arb/tests/test_comparator_content.py b/proof/region/v1/arb/tests/test_comparator_content.py index e0ea968c..9606b9c5 100644 --- a/proof/region/v1/arb/tests/test_comparator_content.py +++ b/proof/region/v1/arb/tests/test_comparator_content.py @@ -45,6 +45,7 @@ import hashlib import sys import unittest +import zlib from dataclasses import fields as dataclass_fields from pathlib import Path @@ -67,13 +68,13 @@ # исходники). Ключ — ИМЯ координаты: перестановка двух координат местами # меняет содержимое поля и краснит вектор. SOURCE_BOUND_PREIMAGE_SHA256_V1 = { - "engine_release": "0b9557439aac7b93eceec78a93d44c00c77a565110fcfe8f90c139bed03b46f1", - "upstream_source": "7d0970948bbc52f91b1211ca985773eabb86007be0ce0d5cb02060e1979af29c", - "arithmetic_input_set": "006a093a27215c5fec14454ac6ec82f008519a437b98001d95d47689d703694d", + "engine_release": "0d85e4a9bd897a9883807c132fc71eebc498a5628b30947e19c6c11383cd7121", + "upstream_source": "1fdceb621a6012a87d427240033264f84916fca01b176ff6587ac1f3e13f5d6b", + "arithmetic_input_set": "7f515cc73adef1d8deb781f2b3dde67fc2c8536f8d1ebe2b18cabf92b0f6b64c", "wrapper_source": "957eb113f89a191ad0eade3cd2f9f3a9a1da021694921a670efa86949e680dc2", "evaluator_source": "9e4f470d56a954a270f636c6c7befca26a77549f345cdaaa83cf35e097952db2", "operation_allowlist": "bae12a908ba87c59c8126811739e85a472218f75c9325d47c2f72f2b7ddf2b79", - "legal_file_set": "5d9b4658d60406d014ddcc7f6ce47966fb795826ce0440d6d0186e283bcc43e1", + "legal_file_set": "69f3375e6918de345c8bc20c484aca831b3eb1d0a85c8c08e30f70520725e7ba", "exclusions": "3c9c249541dced3d1a35159244b4b6feebfa0d290587fdf0cd3c2c98f5941b41", } @@ -466,6 +467,56 @@ def test_every_coordinate_carries_the_domain_separator_that_names_it(self) -> No self.assertEqual(len(labels), 10) +class FixtureArchiveDeterminismTests(unittest.TestCase): + """Reference-вектор обязан стоять на каноническом входе. + + Преимиджи компаратора сворачивают ``archive_sha256`` и замок, а байты + фикстурных архивов синтезирует ``test_pipeline._tar()``. Вывод её + ``gzip.compress(compresslevel=9)`` зависит от версии zlib: Python 3.12.3 + (zlib 1.2.13) и Python 3.14.6 (zlib 1.3.1) выдают разные deflate-байты + для одного входа, а вместе с ними — другие ``archive_sha256`` и другие + преимиджи четырёх координат. Сырой ustar-поток при этом одинаков + (доказано независимым измерением), значит дрейфует только контейнер. + + Закон: фикстурный архив кодируется каноническим RFC 1951 stored-deflate + gzip-членом, чьи байты являются чистой функцией raw tar и не могут + зависеть от zlib интерпретатора. Этот тест — независимый оракул: он + декодирует фактический архив и пересобирает канонический контейнер + самостоятельно, поэтому возврат к ``gzip.compress`` снова краснит его. + """ + + def _canonical_stored_gzip_v1(self, raw: bytes) -> bytes: + header = b"\x1f\x8b\x08\x00" + (0).to_bytes(4, "little") + b"\x00\xff" + body = bytearray() + offset = 0 + while offset < len(raw): + chunk = raw[offset : offset + 65535] + final = 1 if offset + len(chunk) == len(raw) else 0 + body.append(final) + body += len(chunk).to_bytes(2, "little") + body += ((~len(chunk)) & 0xFFFF).to_bytes(2, "little") + body += chunk + offset += len(chunk) + trailer = zlib.crc32(raw).to_bytes(4, "little") + ( + len(raw) & 0xFFFFFFFF + ).to_bytes(4, "little") + return header + bytes(body) + trailer + + def test_fixture_archives_are_canonical_stored_gzip_members(self) -> None: + for salt in ("", "drifted-", "foreign-"): + _lock, admitted = test_pipeline._source_fixture(salt) + for source in admitted.sources: + with self.subTest(salt=salt, archive_sha256=source.archive_sha256.hex()): + raw = zlib.decompress( + source.archive_bytes, + 16 + zlib.MAX_WBITS, + ) + self.assertEqual( + source.archive_bytes, + self._canonical_stored_gzip_v1(raw), + ) + + class SourceBoundReferenceVectorTests(unittest.TestCase): def test_the_vector_covers_exactly_the_source_bound_coordinate_set(self) -> None: # Вектор выводится из протокола, а не переписывается рукой: новая diff --git a/proof/region/v1/arb/tests/test_pipeline.py b/proof/region/v1/arb/tests/test_pipeline.py index abb1c1b4..592ebb1e 100644 --- a/proof/region/v1/arb/tests/test_pipeline.py +++ b/proof/region/v1/arb/tests/test_pipeline.py @@ -3,7 +3,6 @@ from __future__ import annotations -import gzip import hashlib import io import inspect @@ -16,6 +15,7 @@ import tarfile import tempfile import unittest +import zlib from dataclasses import fields as dataclass_fields, replace from functools import cache from pathlib import Path @@ -87,6 +87,34 @@ def _static_elf(payload: bytes = b"fixture") -> bytes: return header + program + body +def _canonical_gzip_v1(raw: bytes) -> bytes: + """Канонический RFC 1951 stored-deflate gzip-член. + + ``gzip.compress(compresslevel=9)`` полагается на deflate-эвристику zlib, + чей вывод менялся между 1.2.13 (Python 3.12) и 1.3.1 (Python 3.14): + одни и те же входные байты давали разные архивы, а с ними другие + ``archive_sha256`` и другие преимиджи source-bound координат. Stored-блоки + копируют вход без сжатия, поэтому байты члена зависят только от самого + tar-потока и не могут разойтись между интерпретаторами. + """ + + header = b"\x1f\x8b\x08\x00" + (0).to_bytes(4, "little") + b"\x00\xff" + blocks = bytearray() + offset = 0 + while offset < len(raw): + chunk = raw[offset : offset + 65535] + final = 1 if offset + len(chunk) == len(raw) else 0 + blocks.append(final) + blocks += len(chunk).to_bytes(2, "little") + blocks += ((~len(chunk)) & 0xFFFF).to_bytes(2, "little") + blocks += chunk + offset += len(chunk) + trailer = zlib.crc32(raw).to_bytes(4, "little") + ( + len(raw) & 0xFFFFFFFF + ).to_bytes(4, "little") + return header + bytes(blocks) + trailer + + def _tar(root: str, files: tuple[tuple[str, bytes, int], ...]) -> tuple[bytes, int]: raw = io.BytesIO() with tarfile.open(fileobj=raw, mode="w", format=tarfile.USTAR_FORMAT) as archive: @@ -108,7 +136,7 @@ def _tar(root: str, files: tuple[tuple[str, bytes, int], ...]) -> tuple[bytes, i member.size = len(body) member.mtime = 0 archive.addfile(member, io.BytesIO(body)) - encoded = gzip.compress(raw.getvalue(), compresslevel=9, mtime=0) + encoded = _canonical_gzip_v1(raw.getvalue()) return encoded, len(raw.getvalue()) diff --git a/proof/region/v1/tests/test_build.py b/proof/region/v1/tests/test_build.py index d82800b0..62f2ef47 100644 --- a/proof/region/v1/tests/test_build.py +++ b/proof/region/v1/tests/test_build.py @@ -54,12 +54,12 @@ def _temporary_mode(path: Path, mode: int) -> Iterator[None]: # Keep an independent outer oracle: importing the gate's expected hash here # would let a coordinated gate edit hide inventory drift. ARB_INVENTORY_SHA256_V1 = ( - "3976521dfa61bb3be30afdaab0ed3066a81ea8c633ba7de413ef07962ed2ab0b" + "e61831a344284a253aa5e292eee97ba9ef0f17f512edd4d868a496c0562818d8" ) ARB_ORDER_SHA256_V1 = ( - "6a86ca077d5ad564c9d253a0e9ac4c59b0165e658a2f8d68e4ee8f63b0fedf60" + "6f6b9bc97311e4ca2c4cbcc312e939cc0ec6ecafca16754d6ea810336530f626" ) -ARB_TEST_COUNT_V1 = 292 +ARB_TEST_COUNT_V1 = 293 MOVED_INPUT_SURFACE_V1 = ( "CanonicalInputLimitsV1", @@ -317,7 +317,7 @@ def test_arb_runtime_binding_propagates_to_downstream_identities(self) -> None: ) self.assertEqual( observed.input_bundle_identity.hex(), - "a6580242b448c88a8f24e61819de47d512ab8fe78cbfe850e7447540e83360e6", + "29abe900a0cf6a989761b4fdca8ebb5181fa6740508318f32a0b1a2aa04296a0", ) self.assertEqual( pipeline.pipeline_policy_identity_v2( @@ -329,7 +329,7 @@ def test_arb_runtime_binding_propagates_to_downstream_identities(self) -> None: self.assertEqual(len(process_bytes), 196) self.assertEqual( hashlib.sha256(process_bytes).hexdigest(), - "d33e0ed28f88e957fbec83679732d325db5f6a893e7ee6058f2d5376a94466cb", + "5922427e8cf732225c21a5e8c19caba945beb3e2bbc05a54fc19155d7e9684e9", ) # One subTest per coordinate: a characterization pin that stops at the # first mismatch hides which part of the chain actually moved, and the @@ -338,32 +338,32 @@ def test_arb_runtime_binding_propagates_to_downstream_identities(self) -> None: ( "comparator.identity", result.comparator.identity.hex(), - "1a17002c015a938f7464d23e4cdc6f567c9aa93ee83201fe2bd33bb8fb3c7a4f", + "8657dd5be2361162b3d4a78044e505c71d52b8234f3be6d569da61b0822408d4", ), ( "evidence.source_identity", result.evidence.source_identity.hex(), - "c34c7c787f23e2f35edc6bdc31b936eb73ffa7a4d5a7ef9c86e9735b7a442cb1", + "1579ae384bc2fb2f7c3bff456ded137f9d20f89f4b46355169bacb992c01393d", ), ( "evidence.build_identity", result.evidence.build_identity.hex(), - "b58d3d95bd73f511a45b23cb3567b4a8fce67f90f929ba2d0ca4359d132b524e", + "f85f5e178ac24bb8a9aa919149c5469656e4e731160ac26692e869629fcd3522", ), ( "evidence.run_identity", result.evidence.run_identity.hex(), - "897560465e8497a1db6061c30dba73d4e604a25b2e810eecd708bea41e9a10e7", + "f820e93e58e8a455759d5f33f5a78d1ae8cd260cb97a44ed52b48bf16d5c5485", ), ( "evidence.identity", result.evidence.identity.hex(), - "f4a4431f2f9a92070e1ad2ada94bf7f9fc83a4b8c7c0d7c26a2e30e80567e2f9", + "b57aaff850ee5a14ad76485b53d7df2c3914a785994d9e5b75ae8f11970a4b76", ), ( "claim.identity", result.claim.identity.hex(), - "05471ad13b98fe588e56d6feadaa21e6ce8c0020ba7428f933974126dcbb365e", + "6eae145733dc5088aa954fc8278d114850464d0941a4a5b9a8a3455f300fd2c9", ), ): with self.subTest(coordinate=name): @@ -2451,7 +2451,7 @@ def test_build_process_encoding_is_total_and_keeps_exact_golden(self) -> None: self.assertEqual(len(encoded), 196) self.assertEqual( hashlib.sha256(encoded).hexdigest(), - "d33e0ed28f88e957fbec83679732d325db5f6a893e7ee6058f2d5376a94466cb", + "5922427e8cf732225c21a5e8c19caba945beb3e2bbc05a54fc19155d7e9684e9", ) forged = tuple.__new__(transport.DockerBuildExitedV1, ()) From 64361ad908602e7d53be7626978804866a176f7b Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:22:47 +0300 Subject: [PATCH 5/5] Review fixes: cache the derived comparator and name the bundle pin origin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit CHANGES_REQUESTED on head c30a8858: _derived_comparator_v1 rebuilt the whole ControlledPipelineV1 comparator on every call, and the re-derived input_bundle_identity/build_process_bytes pins were unexplained. - test_comparator_content.py: decorate _derived_comparator_v1 with functools.cache; the input is deterministic (same fixture closure), so repeated calls now reuse one build observation instead of re-running the pipeline. - test_build.py: comment at the pins — input_bundle_identity folds request.admitted_sources.identity (which commits to each archive_sha256), so re-encoding the fixture archives as canonical stored-deflate gzip members legitimately moved it; input_bundle_sha256 (the USTAR contents hash) is unchanged and proves only the source-lock binding moved, not the bundle bytes. --- proof/region/v1/arb/tests/test_comparator_content.py | 2 ++ proof/region/v1/tests/test_build.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/proof/region/v1/arb/tests/test_comparator_content.py b/proof/region/v1/arb/tests/test_comparator_content.py index 9606b9c5..98e4dcf2 100644 --- a/proof/region/v1/arb/tests/test_comparator_content.py +++ b/proof/region/v1/arb/tests/test_comparator_content.py @@ -42,6 +42,7 @@ from __future__ import annotations +import functools import hashlib import sys import unittest @@ -358,6 +359,7 @@ def render_coordinate_structure_v1( return "\n".join(lines) +@functools.cache def _derived_comparator_v1() -> pipeline.DiagnosticArbComparatorV1: binary = test_pipeline._static_elf(b"derived-comparator") result = pipeline.ControlledPipelineV1( diff --git a/proof/region/v1/tests/test_build.py b/proof/region/v1/tests/test_build.py index 62f2ef47..1b8d1ae4 100644 --- a/proof/region/v1/tests/test_build.py +++ b/proof/region/v1/tests/test_build.py @@ -311,6 +311,14 @@ def test_arb_runtime_binding_propagates_to_downstream_identities(self) -> None: ) self.assertEqual(observed.input_bundle_length, 174_080) + # The sealed bundle carries an opaque binding that folds the source + # closure: `_arb_input_binding_identity_v2` starts from + # `request.admitted_sources.identity`, which commits to each archive's + # `archive_sha256`. The fixture archives were re-encoded as canonical + # RFC 1951 stored-deflate gzip members (zlib-independent), so + # `archive_sha256` moved and this identity had to be re-derived. The + # bundle *contents* did not change — `input_bundle_sha256` below is + # stable — only the source-lock binding moved. self.assertEqual( observed.input_bundle_sha256.hex(), "19b32598d41b021a792e54b807f0143940108055591ca5ef6ecb6a826dec576d",