Skip to content

feat(zk): authenticate the commitment method from the issuer signature - #7082

Merged
jeswr merged 9 commits into
mainfrom
zk/verifier-method-auth
Oct 15, 2026
Merged

jeswr merged 9 commits into
mainfrom
zk/verifier-method-auth

Conversation

@jeswr

@jeswr jeswr commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

🤖 SPARQ agent — verifier-side authentication of a commitment's commitment method from the issuer signature, plus the relying party's value-lane pin functions. Hardening only; the value-lane verifier gates stay CLOSED.

Summary

The verifier used to have to assume which commitment method a committed graph was made under. It now recovers it — and not from the manifest, which the prover writes, but from the issuer signature.

The shape of the argument. sparq-zk-compose::method_auth::candidate_messages builds one candidate signed message per CommitmentMethod::recognised() method, from values the verifier itself has already established: the commitment, the issuer-attested salt, the issuer-bound status reference, and the attested holder digest when the attestation is holder-bound. Alongside them it builds the method-unbound message (ZKSIG_C3/ZKSIG_C4) it computed before. classify then asks which of them the presented signature verifies over:

  • exactly one bound candidate verifies → AuthenticatedMethod::Bound(m);
  • only the unbound one verifies → AuthenticatedMethod::Unbound, which is the explicit absence of an attested method and never a default one;
  • more than one bound candidate verifies → AmbiguousMethod, refused rather than resolved;
  • nothing verifies → NoCandidateVerified, which maps to the pre-existing InvalidIssuerSignature.

Every candidate is verifier-computed. The prover only selects among them; it contributes no message of its own. That is what makes the answer an authentication rather than a hint.

The relying party decides what to do with the answer. MethodPolicy rides on the external trust anchor — KeySet::with_method_policy, following the with_hidden_issuer_depth precedent, so no public verifier signature changes and no policy is ever read from the manifest. MethodPolicy::current() is the default; require_method_bound() refuses an attestation that binds no method (CheckError::MethodNotAuthenticated); an attested superseded DualLeafV1 is refused (CheckError::MethodNotAdmitted) unless the written-down admit_legacy_dual_leaf_v1() opt-in is present; the value-only research dial is admitted by no policy.

Back-compatibility is structural, not a special case. The method-unbound message is itself a candidate, so an already-issued attestation authenticates as Unbound, the default policy accepts it, and authenticated_message(.., Unbound) is byte-identical to the message the verifier computed before. A bad signature still makes nothing verify, so it still reports InvalidIssuerSignature and the refusal precedence is unchanged. Every existing C3/C4 manifest reaches exactly the verdict it reached before, and the whole existing suite passes unedited.

The hidden-only path. A hidden-only commitment has no clear signature to authenticate from. What it does have is the hidden proof's public m, which the bb gate byte-binds — so the method is recovered one step earlier by matching that m against the same verifier-built candidate set. An unmatched m records nothing, the later recompute falls back to the unbound message, and the existing message byte-compare refuses the entry exactly as it does today.

New in sparq-zk: the ZKSIG_M4 message (sig::commitment_message_with_holder_and_scheme) — the one shape binding both a holder key and a commitment method — with its two signers; verify::fragment_typed_filters, a datatype-general sibling of fragment_filters that keeps a FILTER constant as the typed oxrdf::Literal the query spells (one shared private walker; fragment_filters' behaviour and error strings are unchanged, asserted verbatim by a regression test); commit::CommitmentMethod::recognised(); and CommitmentAttestation::from_registry_entry, which bridges a registry entry's already-method-bound signature into a manifest.

Pinned value-lane inputs — and the gates stay closed. sparq-zk-compose::value_pin (behind the OFF-by-default dual-leaf feature) states, for an authenticated DualLeafV2 method and a typed query FILTER, exactly what a value-lane sub-proof's member, lane constant (for the decimal lane, the scale it names, with the query constant rescaled to it exactly and never rounded), op code, bound and verdict would have to be — each equality failing closed with its own PinError. scan_method requires every commitment a scan draws from to have authenticated the same method; check_lane_routed refuses a lane the whole-graph builder does not route.

None of that opens a gate. prefilter_manifest_structure still refuses every value-lane sub-proof unconditionally at stage 0 (ValueLaneSubProofNotAccepted), before it reads a sub-proof for anything else; the stage-2 binding-edge operand match still admits only the filter_int family; filter_edge_true is untouched. A manifest every one of whose pins holds is still refused, and tests/value_lane_pin.rs::a_manifest_whose_pin_passes_is_still_refused asserts exactly that. Nothing ties a pin to an obligation yet: there is no edge from a value-lane sub-proof to a query FILTER, so nothing establishes that a sub-proof whose pin holds is the one answering that FILTER. Computing a pin is not enforcing one; the gate-opening change is separate (sq-j506).

Not externally audited (sq-qhy4); no soundness or privacy property is claimed. No .nr or evidence-pinned file is touched; constraint_pack.py --check and verify_result_evidence.py both pass unchanged.

Mutation results (preflight obligation 1)

Each target was mutated one at a time and reverted; the suite was run in between. Every mutation went red, and the test that died is named.

Mutation Test that died
classify: bound_hits > 1 → > 2 (ambiguity not refused) method_auth::tests::classify_refuses_two_bound_hits_and_reports_nothing_verifying
classify: return Unbound before a bound hit method_auth::tests::classify_prefers_a_bound_hit_and_is_order_independent
MethodPolicy: DualLeafV1 admitted without the opt-in method_auth::tests::an_attested_legacy_method_is_refused_by_policy_not_by_authentication
MethodPolicy::current(): accept_unbound: false (the back-compat default) method_auth::tests::default_policy_accepts_unbound_and_refuses_legacy_v1, verifier::tests::two_credentials_with_distinct_status_refs_are_rejected (+3)
authenticate_hidden_only_method: always Some(Unbound) verifier::tests::a_hidden_only_commitment_authenticates_the_method_its_message_names
Stage-0 ValueLaneSubProofNotAccepted refusal removed value_lane_pin::a_manifest_whose_pin_passes_is_still_refused, no_method_policy_opens_the_value_lane_gate
ZKSIG_M4 domain tag changed sig::tests::the_m4_message_is_poseidon2_over_the_ascii_tag_and_input_order
Pin equality: member value_pin::tests::each_pinned_equality_has_its_own_refusal, numeric_promotion_is_not_supported
Pin equality: lane constant value_pin::tests::each_pinned_equality_has_its_own_refusal
Pin equality: op code value_pin::tests::each_pinned_equality_has_its_own_refusal
Pin equality: verdict true value_pin::tests::each_pinned_equality_has_its_own_refusal
rescale_exact: checked_mul → wrapping_mul value_pin::tests::rescale_exact_is_exact_in_both_directions_and_refuses_overflow
rescale_exact: inexact scale-down admitted value_pin::tests::rescale_exact_is_exact_in_both_directions_and_refuses_overflow, the_decimal_bound_is_rescaled_exactly_to_the_presented_scale
check_lane_routed: every datatype routed value_pin::tests::check_lane_routed_refuses_the_unrouted_seam
scan_method: disagreement tolerated value_pin::tests::scan_method_requires_all_commitments_to_agree

Two of these started out green and are the reason this PR carries two tests it would not otherwise have: the ZKSIG_M4 tag (pairwise distinctness cannot pin it, since M4 is the only six-element preimage in the family, so a re-tagged M4 still collides with nothing) and the hidden-only match (its only coverage was in #[ignore]d live tests, which the default lane does not run). Both mutations are now red.

One mutation is equivalent, not unkilled: rescale_exact's to >= from → to > from. At to == from the else branch computes factor = 10^0 = 1, is_multiple_of(1) holds and m / 1 == m, so the mutant is behaviourally identical to the original and no test can distinguish it.

Base gate (always required)

  • cargo build --workspace succeeds.
  • cargo clippy -p sparq-zk -p sparq-zk-compose --all-targets -- -D warnings clean in both feature states (default, and --features sparq-zk-compose/dual-leaf).
  • The code this PR touches matches the surrounding committed style (cargo fmt --all deliberately not run).
  • cargo test -p sparq-zk -p sparq-zk-compose passes: 696 passed / 0 failed default, 647 passed / 0 failed with dual-leaf. RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features exits 0. markdownlint-cli2@0.18.1 reports 0 errors on the changed markdown.

Targeted re-evaluation (check the rows that apply to your change)

  • Public API (pub item): skills/zk-query-proofs/SKILL.md updated in this change for every new public item, and the surface's tests re-run.
  • ZK verifier / circuits (sparq-zk, sparq-zk-compose, zk/): with nargo + bb on PATH — method_auth_binding 11/11 including the two live #[ignore]d lanes (an end-to-end verify_manifest with an M3 attestation, and a hidden-issuer proof over the M3 message), differential_fuzz 4/4, audit_forge_map 20/20, holder_pop_forge 12/12, dual_leaf_v2_lanes 40/40, dual_leaf_boolean_lane 17/17, dual_leaf_datetime_lane 15/15, value_lane_pin 6/6. No circuit or gate count moves: no .nr file is touched, and the existing dual-leaf feature-matrix leg already has test: true, so the new gated suite runs on CI without a new leg or golden change.

Ratchets and conventions

  • No ratchet lowered. bench/zk-compose/scripts/constraint_pack.py --check and verify_result_evidence.py both pass byte-identical.
  • No hard-coded performance numbers added to markdown.
  • Remaining work is captured as beads (sq-j506 for the gate-opening wiring), not as TODO markers.
  • Doc statements made false by this change were updated in the same change: research/zk-field-native-encoding.md §14.5's precondition now states which of its four conditions are enforced verifier-side and which are not, dispatch.rs's precondition comment likewise, and CHANGELOG.md records all three surfaces.

Security

  • No security regression. This touches sparq-zk and sparq-zk-compose: it removes an assumption from the verifier (the commitment method is authenticated from the issuer signature instead of assumed) and adds a refusal under an opt-in policy, while leaving every existing refusal and its precedence unchanged. The value-lane verifier gates stay closed. Not externally audited (sq-qhy4); no soundness or privacy property is claimed.

🤖 Generated with Claude Code

https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo


Generated by Claude Code

The verifier no longer has to assume which commitment method a committed
graph was made under, and it does not read one from the manifest either --
it RECOVERS it from the issuer signature.

`sparq-zk-compose::method_auth` builds one candidate signed message per
`CommitmentMethod::recognised()` method from values the verifier itself
established (the commitment, the issuer-attested salt, the issuer-bound
status reference, and the attested holder digest when the attestation is
holder-bound) plus the method-UNBOUND message, and `classify` reports
which one the presented signature verifies over. Exactly one bound
candidate verifying names the method; more than one is refused rather
than resolved. The prover only SELECTS among messages the verifier
computed, so it contributes no message of its own.

`MethodPolicy` is the relying party's decision over the answer, carried
on the external trust anchor (`KeySet::with_method_policy`, the
`with_hidden_issuer_depth` precedent, so no public verifier signature
changes). The default accepts a method-unbound attestation, which is what
makes this back-compatible: an existing manifest's `ZKSIG_C3`/`ZKSIG_C4`
attestation authenticates as `Unbound`, the recomputed message is
byte-identical to the one the verifier computed before, and a bad
signature still reports `InvalidIssuerSignature`.

New in `sparq-zk`: the `ZKSIG_M4` message (the one shape binding both a
holder key and a method) with its two signers, `fragment_typed_filters`
for a datatype-general view of a query's FILTER obligations, and
`CommitmentMethod::recognised()`.

`sparq-zk-compose::value_pin` (behind the OFF-by-default `dual-leaf`
feature) states what a value-lane sub-proof's member, lane constant,
scale, op, bound and verdict would have to be for an authenticated
`DualLeafV2` method and a typed query FILTER. **The value-lane verifier
gates stay CLOSED:** the unconditional stage-0 refusal is unchanged and
still first, the stage-2 operand match and `filter_edge_true` are
untouched, and a manifest every one of whose pins holds is still refused
(a test asserts exactly that). Computing a pin is not enforcing one.

Hardening only. Not externally audited (sq-qhy4); no soundness or privacy
property is claimed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo
… register ZKSIG_M4

- The three extended-fragment prefilter tests read Prefiltered.required.
- A hidden-only commitment is tested against the method policy: an attested V1
  message is refused by default and admitted with the opt-in; an unbound
  message is refused under require_method_bound.
- ZKSIG_M4 moves to module scope and joins the pinned domain-tag table.
- The re-spelled lane-constant test keeps a decodable 0x prefix so only the
  canonical-spelling check can refuse it.
- Research note separates the enforced method check from the computed-only
  pins; CHANGELOG states the back-compat claim precisely.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo
@jeswr

jeswr commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

🔎 Codex reviewer — gpt-6.1-sol

Automated review by the Codex reviewer (OpenAI gpt-6.1-sol via Codex CLI) of head 5636f0ead388. Scope: correctness, security, soundness and design; style nits omitted. A new head gets a fresh review.

Findings

  1. [high] Enforce method policy on the hidden-only fallback — crates/sparq-zk-compose/src/verifier.rs:3281
    Hidden-only method admission runs only when classification succeeds. Missing classifications later default to Unbound without applying MethodPolicy, and the declared message used for classification is not checked against the proof-bound message. Consequently, require_method_bound() is not enforced on this fallback.

    Classify the proof-bound message and apply policy to every accepted hidden-only commitment; reject unresolved classifications rather than silently defaulting.

Requested checks

Source review against 47b99884..5636f0e; tests and real proofs were not executed.

  • Classification: exactly one bound hit wins; multiple bound hits return AmbiguousMethod; unbound-only returns Unbound.
  • Policy: carried externally on KeySet; V1 requires opt-in; value-only is never admitted. Hidden-only fallback enforcement has the high-severity defect above.
  • Hidden-only messages: use the same candidate set, but classification and proof-bound verification are insufficiently connected.
  • M4 separation: distinct tag from C1–C4, M2/M3, SI and other signature domains; fixed field ordering, length-separated hashing and length-framed scheme encoding. No cross-format verification problem found.
  • Value pins: member, lane constant, exact decimal rescaling without rounding, operator, bound and true verdict are checked; unrouted lanes are refused.
  • Closed gates: Stage 0 still rejects value-lane sub-proofs before later stages; Stage 2 and filter_edge_true remain closed.

Verdict: Not safe to merge as is: fix the high-severity hidden-only policy gap; no medium findings.

… hidden-only method to the policy

Three hardening changes to verifier-side commitment-method authentication.

1. The relying party's `MethodPolicy` now applies to the method the hidden-issuer
   stage actually binds. A hidden-only commitment's declared `message` must EQUAL
   one of the verifier-built candidate messages: one matching none of them is
   refused in the pre-filter with `HiddenIssuerMessageMismatch` (the same refusal
   the bb stage's own byte-compare raises), and every hidden-only commitment the
   attestation gate accepts now carries an explicit entry in the method map, so
   the absent-key default is not what decides its bound message. Where no
   candidate set can be built at all, the policy is applied to `Unbound`.

2. Domain separation between a REGISTRY record and a PRESENTATION attestation.
   `sig::presentation_message_with_status_and_scheme` adds the `ZKSIG_M5` tag
   (same preimage shape as the registry's `ZKSIG_M3`, own tag), signed by
   `SecretKey::sign_presentation_with_status_and_scheme`; the bearer candidate set
   on the clear and hidden paths uses it, `ZKSIG_M4` still covers the holder-bound
   axis and `ZKSIG_C3`/`ZKSIG_C4` remain the method-unbound candidates. Registry
   signing (`ZKSIG_M2`/`ZKSIG_M3`) is unchanged, and a registry signature
   presented as a clear attestation reports `InvalidIssuerSignature`.
   `CommitmentAttestation::from_registry_entry` is removed: a registry entry is
   not a presentation attestation.

3. Candidate trial-verification is memoized per (commitment, attestation index)
   within one attestation-gate call, and per commitment on the hidden-only path,
   so a commitment many scans reference is verified once. Every other check in
   the loop still runs on every occurrence; `classify` keeps no early exit.

Docs follow the code: who chooses what on each path, that a hidden-only
commitment's recorded method is the one its declared message selects and the bb
stage then holds it to, and an accurate back-compatibility statement (every
manifest that verified before still verifies under the default policy; the newly
accepted ones are those signed over a method-bound presentation message). Not
externally audited (sq-qhy4).

Tests: `ZKSIG_M5` wire-definition KAT and a registry-signature-is-not-a-
presentation pair in `sig.rs`; the same split at the candidate-set level and
through the pre-filter; the unmatched hidden-only message refused under both
policies, structurally and live end to end over a real hidden-issuer proof.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo
…ists

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo

jeswr commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

Head 2ecbb3d5 addresses the review of 5636f0ea.

Hidden-only method (high): authenticate_hidden_only_method now returns a Result.

  • Once a candidate set exists, a declared message that is missing, undecodable or matches no candidate is refused with HiddenIssuerMessageMismatch. An ambiguous match is refused with AmbiguousMethod.
  • MethodPolicy::admit runs on every hidden-only commitment.
  • Every hidden-only commitment the pre-filter accepts gets an explicit method entry, so the method-unbound default no longer decides it.
  • The only remaining no-candidate case is when no status reference is disclosed. The policy is applied there as Unbound, which require_method_bound() refuses. Under the default policy the entry still fails closed later as HiddenIssuerUnreferencedCommitment.
  • Regressions: unit tests, method_auth_binding::an_unmatched_hidden_only_message_is_refused, and a live e2e::hidden_issuer_only_message_is_held_to_the_method_policy built on the C3 hidden-only fixture.

Presentation message:

  • Issuer presentation attestations now sign a new ZKSIG_M5 message (sig::presentation_message_with_status_and_scheme, SecretKey::sign_presentation_with_status_and_scheme).
  • Registry entries keep signing M2/M3, so a registry signature authenticates no presentation method. CommitmentAttestation::from_registry_entry is removed.
  • ZKSIG_M5 is in the pinned tag table with a wire KAT.

Lows:

  • verify_fragment_manifest ordering is left as is. dispatch_fragment runs first by design: it is a pure structural check that runs before the nonce is spent or any bb subprocess starts. The Stage-0 value-lane refusal and the method checks still run, inside verify_manifest_impl. Reordering would change the pinned error ordering for every extended-fragment manifest, so it belongs in its own change.
  • Uniqueness is per signature rather than per commitment, and an issuer and a holder may share a key. Both are noted for the gate-opening work under sq-j506, which is where a method becomes load-bearing.
  • Authentication is now memoised per (commitment, attestation) within a manifest.

Generated by Claude Code

…first

`authenticate_hidden_only_method` read only the FIRST hidden-issuer entry
declared over a hidden-only commitment, so the verdict depended on the order
the prover listed its entries in: [honest, unmatched] passed the pre-filter
while [unmatched, honest] was refused. Every entry over that commitment is now
checked — each must decode and equal a verifier-built candidate, and all of
them must select the SAME candidate, else HiddenIssuerMessageMismatch. The bb
stage already byte-compares every entry against a recomputed message, so this
removes the order dependence without widening what is accepted.

Tests (sparq-zk-compose, tests/method_auth_binding.rs):
- every_hidden_entry_declared_message_is_checked_in_either_order — the two
  orders above, plus two entries that each match a candidate but different
  ones, in both orders.
- each_hidden_only_commitment_gets_its_own_policy_decision — two distinct
  hidden-only commitments under require_method_bound, the first declaring the
  M5 message and the second the unbound one: MethodNotAuthenticated.
- a_hidden_only_commitment_without_a_status_reference_is_refused_when_a_method_is_required
  — no candidate set, so the policy decides on Unbound, through the pre-filter.
- live_additive_clear_m5_attestation_plus_a_hidden_proof_over_the_m5_message
  (#[ignore], nargo/bb) — a clear M5 attestation alongside a hidden proof over
  that same message still verifies end to end under the strict policy.
- sparq-zk sig.rs: M5 and M4 hex known answers alongside C2/C3/M2/M3.

Docs: qualify the back-compat statement in CHANGELOG.md and
skills/zk-query-proofs/SKILL.md (the declared message was previously not read;
a hidden-only manifest whose declared message is absent, non-canonically
spelled or unmatched is now refused, and a refusal may be reported earlier, in
the pre-filter); correct the M3/M4 family note in sparq-zk-compose/src/lib.rs
to M5/M4; place M4 among the method-bound shapes and note in the M5 rationale
that a registry entry signed before the method binding carries a C3 signature
indistinguishable from a C3 presentation attestation (accepted as Unbound by
default, excluded by require_method_bound); state the additive-mode behaviour
on HiddenIssuerAttestation::message; correct Prefiltered::methods on a
hidden-only commitment with no resolvable salt.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo
…ssage its proof is checked against

The hidden-issuer stage now refuses, before decoding any proof, an entry
whose declared message differs from the recomputed one, including over a
commitment that also has a clear attestation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo
…essage qualification precisely

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo

jeswr commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

Merging on the stand-in gates while the automated reviewer is offline. Both gates ran on a5073961. The delta from there to 1be08d74 is docs and a test move only.

  • Independent review of the full fix delta: no correctness or security defects. Its doc findings are fixed in 1be08d74.
  • Separate adversarial review at maximum effort: clean, with no high or medium findings. It included real bb proofs for additive and hidden-only entries, probes for spelling and order, and a precedence check.

ci-fast is green on 1be08d74. The automated reviewer will review the merged diff when it is back. The value-lane gates stay closed.


Generated by Claude Code

@jeswr
jeswr merged commit 34b280c into main Oct 15, 2026
10 checks passed
@jeswr
jeswr deleted the zk/verifier-method-auth branch October 15, 2026 01:19
@jeswr

jeswr commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

🔎 Codex reviewer — gpt-6.1-sol

Automated review by the Codex reviewer (OpenAI gpt-6.1-sol via Codex CLI) of head 1be08d7431dc. Scope: correctness, security, soundness and design; style nits omitted. A new head gets a fresh review.

Findings

No correctness, security, soundness or design problems found.

Requested checks

  • Method authentication: exactly one bound hit selects its method; multiple bound hits return AmbiguousMethod; unbound-only returns Unbound; no hit rejects.
  • Policy: supplied externally through KeySet. Legacy V1 requires explicit opt-in; value-only is never admitted; require_method_bound() rejects unbound attestations.
  • Hidden entries: hidden-only classification returns Result, checks every matching declaration against the same candidates, and requires agreement. Unmatched or undecodable messages reject. The later pre-pass checks additive and duplicate declarations against the recomputed message before decoding hidden proofs.
  • Memoization: confined to one call. Clear authentication uses canonical commitment plus attestation index; the remaining authentication inputs are fixed within that call. Distinct commitments cannot share cached classifications.
  • Message separation: M4/M5 have distinct tags from C2/C3/C4/M2/M3/SI and the other listed domains. Poseidon includes input length; IRI encodings include byte length. Registry M3 is excluded from presentation candidates, and from_registry_entry is absent.
  • Value pins: check member, lane constant, exact decimal rescaling, operator, bound and true verdict; unrouted lanes reject. Stage-0 refusal remains ahead of subsequent verification, and value-lane obligation gates remain closed.
  • Regression coverage: inspected the C3 fixture regression, hidden-entry ordering and additive tests, and M4/M5 known-answer tests. Tests were not executed in this read-only workspace.

Verdict: Safe to merge as is based on static review; no medium or high findings.

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.

2 participants