Skip to content

Proof: one process that mints both receipts is the only place a dual proof can be sealed (V5b2d-4d) - #549

Merged
lemone112 merged 8 commits into
mainfrom
v5b2d-4d-dual-proof-gate
Aug 7, 2026
Merged

Proof: one process that mints both receipts is the only place a dual proof can be sealed (V5b2d-4d)#549
lemone112 merged 8 commits into
mainfrom
v5b2d-4d-dual-proof-gate

Conversation

@lemone112

Copy link
Copy Markdown
Collaborator

Что это

Механизм критерия выхода V5b2d №3: место, где полнодоменное дуальное доказательство вообще может быть запечатано.

join_dual_proof_v1 принимает пять цепей доказательств. Две из них — source-bound квитанции движков — не имеют wire-формы намеренно: квитанция, разбираемая из байтов, позволила бы чужому коду начеканить провенанс. Отсюда следует, что валидной двух-job топологии не существует в принципе: join возможен только внутри процесса, который сам начеканил обе квитанции.

Этот PR добавляет такой процесс.

Состав

  • proof/region/v1/tests/dual_proof_gate.py — собирает и запускает оба движка над полным манифестом подряд, пересобирает семантические квитанции из полос верификации и печатает дуальное доказательство.
  • .github/workflows/dual-proof.yml — один job под этот гейт: объединённое замыкание исходников, оба OCI-манифеста, один делегированный cgroup, покрытие полос из указанного прогона.
  • seal_full_domain_receipt_v1 в обоих модулях полос — гейт чеканит ту же квитанцию, что и полоса, вместо повторного описания координат сборки. Две копии разошлись бы, и расхождение вылезло бы как посторонний отказ допуска через час нативного прогона.
  • proof/region/v1/tests/test_decorator_placement.py — гейт против класса дефекта, который эта же правка и внесла (см. ниже).
  • PROTOCOL.md — раздел «Где может быть запечатано дуальное доказательство».

Почему полосы прошлого прогона допускаются свежими квитанциями

Ровно из-за #547: полоса связывает source-идентичность компаратора, а она воспроизводится между раннерами. Полная идентичность сворачивает наблюдение сборки и не воспроизводится — полоса, связанная с ней, умерла бы вместе с прогоном, давшим ей evidence.

Та же source-идентичность разделяет покрытие по движкам: принадлежность полосы выводится из её манифеста, а не из имени артефакта. Первая версия читала выдуманный ключ comparator_kind, которого в схеме нет.

Замерено, а не предположено

Утверждение Замер
один job может обслужить оба движка без коллизии переменных замки движков пинят побайтно одинаковые GMP и MPFR; расхождение в будущем даёт выход 64, а не тихий выбор одного
последовательная пара влезает в конверт job'а на прогоне 31116022208 нативные RUN заняли 63 мин (Arb) и 43 мин (MPFI)
два исполнения в одном процессе не сталкиваются cgroup именуется labcolors-executor-{pid}-{счётчик} и снимается в close()
мои файлы не сдвигают source-идентичность объявленное замыкание — узкий набор (evaluator C/H, build-скрипты, formula.py); подтверждено боевым замером: между прогонами с влитым #547 source-идентичность MPFI совпала

Гейт не умеет проходить молча

При неполной среде тест падает, а не пропускается: модуль вызывается только намеренно, поэтому тихий проход означал бы доказательство, которого не было. Проверено локально — выход 1 с перечнем недостающих переменных.

Дефект, который эта же правка внесла, и закрытый класс

Вынос помощника поставил его между @unittest.skipUnless и классом, который этот декоратор охранял. Разом два дефекта: помощник стал невызываемым (любой вызов → SkipTest), а нативный тест лишился гейта, не пускавшего его в среду, которая его не тянет.

Ни проверка синтаксиса, ни быстрый набор этого не увидели — модули полос лежат вне инвентаря test_*.py. Всплыло бы только отказом в диспатченном job'е через час.

test_decorator_placement.py читает дерево как исходники (как соседний гейт арности) и запрещает unittest.skip* на модульной функции. Доказательство чувствительности: гейт зелёный на исправленном дереве и находит ровно этот дефект на реконструкции реального сломанного файла.

Проверки

  • Полный быстрый набор: 260 тестов, 7 ошибок — ровно известная база Windows (слой arb/mpfi не импортируется без fcntl). До правки было 258/7. Новых ошибок нет.
  • Гейт размещения декораторов: зелёный на дереве, красный на реконструкции дефекта.
  • Чувствительность dual-proof гейта к неполной среде: выход 1.
  • YAML workflow разбирается, 11 шагов.
  • Настоящая проверка — только CI: локально нативный слой не поднимается.

Чего этот PR НЕ делает

Не запускает дуальное доказательство. Для этого нужны 512 полос верификации против evidence-прогона; они ещё не диспатчены. Этот PR даёт механизм, а не результат.

Откат

Ревертом одного коммита. Ничего существующего не переиспользуется, кроме двух вынесенных функций; их прежние вызывающие — те же тесты полос, покрытые быстрым набором.

…proof can be sealed (V5b2d-4d)

`join_dual_proof_v1` needs five evidence chains, and two of them — the
engines' source-bound receipts — have no wire form by design: a receipt that
could be parsed from bytes would let foreign code mint provenance.  So the
join has no valid two-job topology at all; it can only happen inside a
process that minted both receipts itself.

This adds that process.  `tests/dual_proof_gate.py` builds and runs both
engines over the full manifest back to back, re-seals each engine's semantic
receipt from the verification lanes, and seals the dual proof.  The lane
cover comes from an earlier run and admits here only because a lane binds the
comparator's source identity (#547); the same identity sorts the cover by
engine, so nothing trusts an artifact's name.

Both engine lane modules now expose `seal_full_domain_receipt_v1`, so the
gate mints the same receipt those lanes do instead of restating the build
coordinates — two copies would drift, and the drift would surface as an
unrelated admission failure an hour into a native run.

The gate fails, never skips, on an incomplete environment: it is only ever
invoked deliberately, so a silent pass would be a proof that did not happen.

Measured, not assumed:
  - both engines' source locks pin byte-identical GMP and MPFR, so one job
    can serve both without an env collision; a future divergence exits 64
  - the native RUNs took 63 min (Arb) and 43 min (MPFI) on run 31116022208,
    so the sequential pair fits one job's envelope
  - executor cgroups are named per process with a counter and removed on
    close, so two sequential executions do not collide

`test_decorator_placement.py` closes the class of defect this change itself
introduced: extracting the helper put it between the module's env gate and
the class that gate guarded, which silently made the helper uncallable and
left the native test ungated.  Syntax checks and the fast suite both saw
nothing — the lane modules live outside the `test_*.py` inventory.  The new
gate reads the tree as source, like the arity gate beside it.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2d55bbd7-9703-4443-a4c2-353781086056

📥 Commits

Reviewing files that changed from the base of the PR and between e420e21 and e166c72.

📒 Files selected for processing (12)
  • .github/workflows/dual-proof.yml
  • .github/workflows/verification-lanes.yml
  • proof/region/v1/PROTOCOL.md
  • proof/region/v1/arb/tests/full_domain_receipt.py
  • proof/region/v1/arb/tests/gate.py
  • proof/region/v1/executor.py
  • proof/region/v1/mpfi/tests/full_domain_receipt.py
  • proof/region/v1/tests/dual_proof_gate.py
  • proof/region/v1/tests/test_build.py
  • proof/region/v1/tests/test_decorator_placement.py
  • proof/region/v1/tests/test_executor.py
  • proof/region/v1/tests/test_workflow_inputs.py

Comment @coderabbitai help to get the list of available commands.

@lemone112
lemone112 marked this pull request as draft August 6, 2026 18:02
@lemone112

Copy link
Copy Markdown
Collaborator Author

Два независимых ревью финального состояния нашли блокеры. Перевожу в черновик: в текущем виде воркфлоу не может запечатать доказательство ни разу.

Блокер 1 — containment. После первого движка процесс навсегда остаётся в proof/observer под pids.max=2: в непроверочном коде ровно две записи в cgroup.procs (executor.py:1529 вход в observer, executor.py:1634 привязка дочернего), обратной миграции нет. Сборка второго движка форкает docker CLI (Go-рантайм сразу заводит потоки, которые pids-контроллер считает задачами) и упирается в лимит — через час нативного прогона. Эталоны работают потому, что arb.yml держит движки в РАЗНЫХ шагах (разные процессы), а full-domain-run.yml — в разных job'ах. Один процесс на два движка встречается впервые здесь, и контракт observer его не переживает.

Блокер 2 — доставка покрытия. Полоса = один dispatch = один прогон = один артефакт, то есть покрытие 2^24 распределено по 512 прогонам, а download-artifact берёт из одного run-id. Плюс имя артефакта не различало движки, поэтому полосы Arb и MPFI одного окна претендовали на один каталог. Плюс опечатка: вход объявлен lane_run_ids, использовался lane_run_id.

Прочее: тавтологичная антивакуумная проверка (identity квитанции не кодирует покрытие вообще), неполный предполётный список переменных, непроверенный union-возврат seal_full_domain_receipt_v1, потеря враждебной проверки replay_evidence_is_well_bound_v1, отсутствие контрактного теста на сам воркфлоу.

Чиню; переоткрою после исправлений и повторного независимого ревью.

Claude Code added 6 commits August 6, 2026 21:09
…review

Two independent reviews of the final state found that the job this branch
added could never have sealed anything.  Both are fixed here.

CONTAINMENT.  The controller that observes a BUILD to RUN enters the observer
group and stays there: non-test code writes `cgroup.procs` in exactly two
places — the observer placement and the executed child's attach — and neither
walks it back.  With one shared subtree at `pids.max=2`, the second engine's
BUILD forks docker into a saturated budget and dies, an hour into the run.
The single-engine lanes never met this because `arb.yml` puts the engines in
separate steps and `full-domain-run.yml` in separate jobs; one process for
both engines happens first here, and the dual proof admits no other topology.

So each engine now gets its own observer subtree, and `enter_task_cgroup_v1`
returns the controller to the unconstrained group between them — where a
fresh process would have started.  No run's containment widens: the observer
budget still governs each execution.

DELIVERY.  A lane is one dispatch, one run, one artifact, so a full-domain
cover is spread across 512 runs while the job read a single `run-id` — and
the input was declared `lane_run_ids` but read as `lane_run_id`.  Worse, lane
artifacts carried no engine in their name, so an Arb lane and its MPFI twin
claimed one name and could not coexist under one root.  The cover now spans
many runs through the shape `full-domain-corpus.yml` already uses, and the
artifact name carries the engine.  Which engine a lane serves is still
decided by the comparator source identity in its manifest, never by a name.

A cover that is short, overlapping, or single-engine is now refused in
seconds, before the first build, instead of after both native runs.

Also from review:
  - the anti-vacuity check was theatre: receipt identity seals job,
    comparator, run and transcript and never the cover, so two engines could
    not collide in it by construction.  Replaced with the property it claimed
    — the two covers are disjoint sets of lane directories.
  - the preflight listed 4 of the 10 variables the seal functions read, so a
    partial environment surfaced as a bare KeyError instead of the refusal
    this gate promises.
  - `seal_full_domain_receipt_v1` returns a union; an unchecked rejection
    became `AttributeError` two hours in, losing the reason.  Typed now.
  - the gate skipped the hostile `replay_evidence_is_well_bound_v1` check its
    single-engine lanes run, and this is the only place a full-domain receipt
    is ever sealed.  Restored, with the transcript's domain and comparator
    coordinates.
  - module resolution across the engines' five colliding stems was correct
    only by the order of two loads.  Asserted instead of assumed.

Verified: 260 local tests, 7 errors — the exact Windows baseline, unchanged.
The new `enter_task_cgroup_v1` tests cannot run here at all: `test_executor`
needs `fcntl`.  CI is the only place they execute.
The dispatch workflows are pinned by the coordinates they must carry, which
catches a missing coordinate but not a misspelt one — and a misspelt one is
what happened: the dual-proof input was declared `lane_run_ids` and read as
`lane_run_id`.  GitHub expands an undeclared input to the empty string, so
nothing fails until the job has run.  On this job that is two hours of native
execution before noticing the lane cover was empty.

`test_every_referenced_input_is_declared` closes the class for every workflow
at once, reading the declarations by indentation rather than parsing: the
proof tree carries no YAML dependency.  Proven red on the real drift, not a
mock — reintroducing exactly `inputs.lane_run_id` into the live workflow
makes the gate name it undeclared, and the fixed file is clean.  It covers
three workflows and eleven references today, with an anti-vacuity floor so a
gate that stopped finding anything cannot pass quietly.

`DualProofContainmentContractTests` pins the two properties whose absence made
the job unrunnable: each engine owns its observer subtree (a shared one admits
two tasks, and the second engine's BUILD forks into it), and the cover is
gathered from many runs rather than one `run-id`.  It also pins that the cover
is checked before the first build, so the cheap refusal cannot drift back
behind the expensive work.
Re-review of the final state found that the previous commit would have failed
CI deterministically and that one of its own repairs repeated the defect it
replaced.

THE RED.  `arb/tests/gate.py` pins the exact inventory of the fast suite, and
that suite includes `tests/test_executor.py` through its shared patterns.
Adding three tests there moved the hash and left the pin behind, so the first
step of `arb.yml` would have printed inventory drift and returned 1 before
building anything.  Recomputed independently with an AST replica of the gate's
own algorithm — the replica reproduces the parent commit's pinned value byte
for byte, which is what makes its answer for HEAD trustworthy: 270 tests,
b1bc948c…  None of this is observable here; the gate needs `fcntl`, so the
machine that wrote the drift could not run the check that catches it.

THE SECOND TAUTOLOGY.  Replacing the theatre assert with "the two covers are
disjoint" was theatre again: `_lane_cover_v1` partitions by comparator source
identity, and the two engines cannot share one — the sets are disjoint by
construction, and the line could not fail in any state the gate reaches.  What
is genuinely unguaranteed is that the partition consumed the whole cover: a
lane of a third identity is foreign to both engines and disappears silently.
The union of both covers must now equal every lane directory present.

Also from review:
  - the MPFI hostile check was vacuous — that receipt's constructor already
    requires the predicate, so the assert could not fail.  Only Arb's remains,
    where the constructor binds something narrower.  The previous commit
    claimed to restore both; it restored one.
  - `enter_task_cgroup_v1` took the caller's word for where it landed.  The
    observer placement is always followed by a budget probe; this one now
    verifies its own post-condition, because the path arrives from
    configuration and a wrong one silently moves where the next BUILD runs.
  - the module-collision guard treated an absent import as safety.  Absence
    retires the check instead of failing it, so it is a refusal now.
  - PROTOCOL.md still called the observer placement the only versioned
    cross-module placement operation.  There are two, and the second is
    documented with why it cannot widen a run's containment.

Verified: 265 local tests, 7 errors — the exact Windows baseline, unchanged.
The recomputed inventory now equals the pin.
A full-domain cover is 512 separate runs, and the dual proof needs their ids.
Without a name carrying the coordinates the only way to collect them is a
guess about creation times, which breaks the moment two campaigns overlap.
The run name makes the list a query — and makes a lane identifiable in the
UI, where 512 identically-named runs are otherwise indistinguishable.
… does

The post-condition added a moment earlier compared paths by spelling, while
every other placement check in this module compares by inode — `fstat` on both
descriptors — precisely because one group can be reached by more than one
name.  A false refusal here is not a cheap failure: it aborts a job two hours
into two native runs, which is the same cost as having no check at all.
…howed

The last two rounds shipped a red CI each because the checks that catch
inventory drift cannot run on Windows — and WSL, a real Linux where this whole
layer imports, was available the entire time.  Everything below was measured
by running it there, not derived.

MY OWN TEST WAS RED.  The post-condition added last round refuses when the
process is not in the named group, and the test I wrote for that placement
builds a fake cgroup in a temporary directory — where no fixture can move this
process.  It now patches the current-cgroup reader the way the observer probe
test beside it already does, and the post-condition finally has a test that
proves it fires: an invariant added without one was exactly the omission the
reviews keep finding.

THE SECOND PIN.  `tests/test_build.py` keeps an independent outer oracle of
the same inventory — count, inventory digest and discovery-order digest — on
purpose, so a coordinated edit to the gate cannot hide drift.  Updating only
the gate left it stale, and the oracle did its job by failing.  All three
constants now carry values the gate actually printed on Linux: 271 tests,
inventory 86c723aa…, order fe5a3419…  The order digest is over discovery order
and cannot honestly be derived by hand; it was read off a run.

Both gates verified green there: the fast Arb gate reports 271 tests with its
exact 15-skip manifest, and the outer oracle passes.

PROTOCOL.md's new paragraph was mechanically spliced — a 200-column line, an
orphaned clause whose "this descriptor protocol" had drifted onto the wrong
subject, and wording describing a string comparison the code no longer does.
Rewritten as its own paragraph.

Verified on Linux: 419 tests, 3 failures.  All three are pre-existing and
mine only by proximity — I reproduced them on a worktree whose proof tree is
untouched `main`, where the same three fail identically.  Windows still gives
265 tests and 7 load errors; that is the platform's ceiling, not a result.
@lemone112 lemone112 closed this Aug 6, 2026
@lemone112 lemone112 reopened this Aug 6, 2026
@lemone112
lemone112 marked this pull request as ready for review August 6, 2026 20:29
@lemone112

Copy link
Copy Markdown
Collaborator Author

Оба блокера прошлого ревью закрыты, поэтому вывожу из черновика. Что именно проверено и чем:

Блокер 1 — мой собственный тест был красным. Пост-условие enter_task_cgroup_v1 отвергает случай «процесс не попал в названную группу», а фикстура моего же теста создаёт cgroup во временном каталоге, куда процесс попасть не может. Теперь тест подменяет чтение текущей группы (как уже делает соседний тест наблюдателя), и у самого пост-условия появился тест, доказывающий, что оно срабатывает. Сравнение переведено на иноду, а не на написание пути: два имени одной группы не должны стоить двухчасового прогона.

Блокер 2 — второй пин инвентаря. tests/test_build.py держит независимый внешний оракул того же инвентаря намеренно, чтобы согласованная правка гейта не могла спрятать дрейф. Я обновил только гейт. Это подтверждено боевым CI: прогон коммита c2ecc49 упал с AssertionError: 270 != 267 — ровно этот оракул. Теперь все три константы несут значения, которые гейт напечатал при исполнении: 271 тест, инвентарь 86c723aa…, order fe5a3419…. Order-хеш считается по порядку discovery и честно выводится только прогоном.

Как проверено. Ключевое: эти два гейта на Windows не запускаются вовсе (нужен fcntl), и именно поэтому дрейф уезжал дважды. Перенёс проверку в WSL — это настоящий Linux, где слой импортируется:

  • быстрый Arb-гейт: OK (skipped=15), 271 тест, точный манифест пропусков;
  • внешний оракул ExistingArbGateTests: OK;
  • полный набор: 419 тестов, 3 падения — и я воспроизвёл те же три на дереве с нетронутым proof-слоем, то есть это среда, а не регрессия.

Незакрытый гейт называю прямо: независимого ревью состояния после d6b4d8f ещё не было. Три круга ревью прошли более ранние состояния, и каждый круг находил дефект в предыдущей починке — поэтому четвёртый круг здесь не формальность.

@lemone112 lemone112 closed this Aug 6, 2026
@lemone112 lemone112 reopened this Aug 6, 2026
…ded last

The cover is gathered from many runs into one directory, so a re-run of the
same window overwrites the evidence already there.  The result still looks
exact — the window is present once, the arithmetic adds up — while one of two
answers silently won, and nothing records which.  Each run now lands in its
own staging directory and the move into the flat cover refuses a name that is
already taken.

Lane run ids are validated as numbers, the way `verification-lanes.yml`
already validates its own: an element starting with `-` would be read by `gh`
as a flag rather than a run.

Both are pinned by the workflow contract tests, so they cannot drift back.

Verified: 7 contract tests pass; the workflow still parses to 11 steps.

Left open and named rather than hidden: the cheap preflight proves the cover
is two exact domains, but not that those two identities belong to the engines
this job is about to build.  Closing that needs the comparator's source
identity derived from the checkout without building — the coordinates are all
static, but no such derivation is exposed today.
@lemone112 lemone112 closed this Aug 7, 2026
@lemone112 lemone112 reopened this Aug 7, 2026
@lemone112
lemone112 merged commit dc14541 into main Aug 7, 2026
10 checks passed
@lemone112
lemone112 deleted the v5b2d-4d-dual-proof-gate branch August 7, 2026 06:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant