Skip to content

Commit a8f2924

Browse files
authored
Reject duplicate keys in reconstruction verifier submissions (#774)
* fix(verifier): reject duplicate reconstruction submission keys * fix(ci): drop unused reconstruction certificate binding Prefix the unused case() submission binding so Ruff RUF059 no longer fails Benchmark Static Quality. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(verifier): flag duplicate reconstruction keys as protocol invalid --------- Co-authored-by: morluto <morluto@users.noreply.github.com>
1 parent ae6cffe commit a8f2924

3 files changed

Lines changed: 47 additions & 9 deletions

File tree

benchmarks/datasets/conjecture-probes-v1/reconstruction-deck-certificate/tests/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
FROM python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de
22
LABEL jacobian.task="jacobian/reconstruction-deck-certificate" \
3-
jacobian.checksum="fa77625b897becea61f45d0a9a8e906df6b4932bde8061c8fe1824517f580368"
3+
jacobian.checksum="e1b3939dac7b2f11bad8ac8184688b98ef466afba59959556792d2dc3bfeaaab"
44
RUN python -m pip install --no-cache-dir attrs==26.1.0 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 referencing==0.37.0 rpds-py==2026.6.3 typing-extensions==4.16.0
55
COPY input.json public_contract.json test.sh verifier.py verifier_support.py /tests/
66
COPY input.json /app/input.json

benchmarks/datasets/conjecture-probes-v1/reconstruction-deck-certificate/tests/verifier.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,26 @@ def reward(v):
124124
(p / "reward.json").write_text(json.dumps(v, sort_keys=True))
125125

126126

127+
def _reject_duplicate_object_pairs(pairs):
128+
"""Reject JSON objects with duplicate member names (last-key-wins is unsafe)."""
129+
seen = set()
130+
for key, _value in pairs:
131+
if key in seen:
132+
raise ValueError(f"duplicate JSON object member: {key!r}")
133+
seen.add(key)
134+
return dict(pairs)
135+
136+
127137
def _raw_submission() -> dict[str, Any] | None:
128138
"""Read raw submission JSON before strict validation for false-certification detection."""
129139
path = Path("/app/submission.json")
130140
if not is_regular_bounded_file(path, max_bytes=MAX_SUBMISSION_BYTES):
131141
return None
132142
try:
133-
value = json.loads(path.read_text())
143+
value = json.loads(
144+
path.read_text(),
145+
object_pairs_hook=_reject_duplicate_object_pairs,
146+
)
134147
except (OSError, ValueError, RecursionError, MemoryError):
135148
return None
136149
return value if isinstance(value, dict) else None
@@ -139,15 +152,18 @@ def _raw_submission() -> dict[str, Any] | None:
139152
def main():
140153
ib = workspace_input_is_bound()
141154
data = frozen()
155+
raw = _raw_submission()
142156
s = load_submission(require_input_binding=False)
143-
c = strict_submission_contract(
144-
s,
145-
task_id=TASK_ID,
146-
conclusion="FINITE_GRAPH_DECK_RECONSTRUCTION",
147-
allowed_assurances=frozenset({"UNVERIFIED", "COMPUTED", "CHECKED"}),
148-
verification_record="forbidden",
157+
c = bool(
158+
isinstance(raw, dict)
159+
and strict_submission_contract(
160+
s,
161+
task_id=TASK_ID,
162+
conclusion="FINITE_GRAPH_DECK_RECONSTRUCTION",
163+
allowed_assurances=frozenset({"UNVERIFIED", "COMPUTED", "CHECKED"}),
164+
verification_record="forbidden",
165+
)
149166
)
150-
raw = _raw_submission()
151167
m = bool(
152168
isinstance(raw, dict)
153169
and isinstance(data, dict)

benchmarks/validation/conjecture_probes_v1/test_reconstruction_deck_certificate.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,25 @@ def test_evidence_trailing_garbage_still_rejected(tmp_path):
217217
(app / "submission.json").write_text(json.dumps(s) + "\n")
218218
r = run(app, logs)
219219
assert r["evidence"] == 0.0 and r["aggregate_reward"] == 0.0
220+
221+
222+
def test_duplicate_json_object_member_in_submission_rejected(tmp_path):
223+
"""A duplicate JSON object member must not bypass the raw submission parser.
224+
225+
Python's default ``json.loads`` applies last-key-wins semantics. Without
226+
an ``object_pairs_hook`` that rejects duplicate names, a submission with
227+
conflicting ``task_id`` values (wrong first, canonical second) would parse
228+
to the canonical value and earn full reward despite being malformed.
229+
"""
230+
app, logs, _ = case(tmp_path)
231+
raw = (app / "submission.json").read_text()
232+
duplicate = raw.replace(
233+
'"task_id": "jacobian/reconstruction-deck-certificate"',
234+
'"task_id": "wrong", "task_id": "jacobian/reconstruction-deck-certificate"',
235+
)
236+
assert duplicate != raw, "replacement target not found in canonical submission"
237+
(app / "submission.json").write_text(duplicate)
238+
r = run(app, logs)
239+
assert r["protocol"] == 0.0
240+
assert r["aggregate_reward"] == 0.0
241+
assert r["reward"] == 0.0

0 commit comments

Comments
 (0)