perf(m49): verify source payloads during assembly

This commit is contained in:
DCCONSTRUCTIONS
2026-08-31 19:06:59 +03:00
parent bfc1f1bbed
commit 35d99e40d5
4 changed files with 154 additions and 26 deletions
@@ -34,7 +34,7 @@ from k1link.observatory.m49_portable_source import (
M49_PORTABLE_TGS_SEQUENCE, M49_PORTABLE_TGS_SEQUENCE,
M49PortableSourceStage, M49PortableSourceStage,
materialize_m49_portable_source_from_worker_stage, materialize_m49_portable_source_from_worker_stage,
validate_m49_portable_source_stage, validate_m49_portable_source_stage_binding,
) )
from k1link.observatory.portable_result_contract import ( from k1link.observatory.portable_result_contract import (
OBSERVATION_ONLY_AUTHORITY, OBSERVATION_ONLY_AUTHORITY,
@@ -249,7 +249,7 @@ class M49PortableProfileRunnerAdapter:
) )
job = source.job job = source.job
_verify_plan(plan, job=job, definition=self.definition) _verify_plan(plan, job=job, definition=self.definition)
stage = validate_m49_portable_source_stage(source.root) stage = validate_m49_portable_source_stage_binding(source.m49_stage)
_verify_installation_unchanged(self.installation) _verify_installation_unchanged(self.installation)
workspace = Path( workspace = Path(
tempfile.mkdtemp(prefix=".m49-portable-run-", dir=self.installation.output_parent) tempfile.mkdtemp(prefix=".m49-portable-run-", dir=self.installation.output_parent)
@@ -268,7 +268,7 @@ class M49PortableProfileRunnerAdapter:
timeout_seconds=self.installation.timeout_seconds, timeout_seconds=self.installation.timeout_seconds,
) )
package = assemble_m49_portable_result( package = assemble_m49_portable_result(
source_stage_root=stage.root, source_stage=stage,
runner_output_root=output, runner_output_root=output,
runner_timing_path=timing, runner_timing_path=timing,
profile_path=self.installation.profile_path, profile_path=self.installation.profile_path,
+35 -10
View File
@@ -33,8 +33,9 @@ from k1link.observatory.m49_portable_source import (
M49_PORTABLE_PROFILE_SCHEMA, M49_PORTABLE_PROFILE_SCHEMA,
M49_PORTABLE_STAGE_INDEX, M49_PORTABLE_STAGE_INDEX,
M49_PORTABLE_STAGE_MANIFEST, M49_PORTABLE_STAGE_MANIFEST,
M49PortableSourceStage,
read_m49_source_index, read_m49_source_index,
validate_m49_portable_source_stage, validate_m49_portable_source_stage_binding,
) )
from k1link.observatory.portable_result_contract import ( from k1link.observatory.portable_result_contract import (
OBSERVATION_ONLY_AUTHORITY, OBSERVATION_ONLY_AUTHORITY,
@@ -146,7 +147,7 @@ class _TimingRow:
def assemble_m49_portable_result( def assemble_m49_portable_result(
*, *,
source_stage_root: Path, source_stage: M49PortableSourceStage,
runner_output_root: Path, runner_output_root: Path,
runner_timing_path: Path, runner_timing_path: Path,
profile_path: Path, profile_path: Path,
@@ -157,14 +158,17 @@ def assemble_m49_portable_result(
) -> M49PortableResultPackage: ) -> M49PortableResultPackage:
"""Assemble one exact content-addressed portable result package. """Assemble one exact content-addressed portable result package.
The input ``job`` is either the running durable queue record or its exact ``source_stage`` is a previously admitted, content-addressed stage. Its
path-free Worker projection. The returned manifest digest and result ID small manifest, schedule, and index are rebound here after the external
are the two values the Worker later supplies to the queue's success runner returns; each large point file is digest-checked during the required
transition. assembly read. The input ``job`` is either the running durable queue record
or its exact path-free Worker projection. The returned manifest digest and
result ID are the two values the Worker later supplies to the queue's
success transition.
""" """
_verify_running_job_definition(job, definition) _verify_running_job_definition(job, definition)
source_stage = validate_m49_portable_source_stage(source_stage_root) source_stage = validate_m49_portable_source_stage_binding(source_stage)
_verify_source_stage_job_binding(source_stage.root, job) _verify_source_stage_job_binding(source_stage.root, job)
profile = _read_profile(profile_path, definition) profile = _read_profile(profile_path, definition)
source_index_path = source_stage.root / M49_PORTABLE_STAGE_INDEX source_index_path = source_stage.root / M49_PORTABLE_STAGE_INDEX
@@ -228,6 +232,8 @@ def assemble_m49_portable_result(
*PurePosixPath(cast(str, source["relative_path"])).parts *PurePosixPath(cast(str, source["relative_path"])).parts
), ),
label="materialized TGS input", label="materialized TGS input",
expected_byte_length=cast(int, source["byte_length"]),
expected_sha256=cast(str, source["sha256"]),
) )
ground = _load_xyzi( ground = _load_xyzi(
output_root / f"{frame_index}_ground.bin", output_root / f"{frame_index}_ground.bin",
@@ -1210,11 +1216,30 @@ def _read_existing_package(root: Path) -> PortableResultPackageManifest:
return package return package
def _load_xyzi(path: Path, *, label: str) -> npt.NDArray[np.float32]: def _load_xyzi(
path: Path,
*,
label: str,
expected_byte_length: int | None = None,
expected_sha256: str | None = None,
) -> npt.NDArray[np.float32]:
candidate = _safe_file(path, label) candidate = _safe_file(path, label)
if candidate.stat().st_size % 16: byte_length = candidate.stat().st_size
if (expected_byte_length is None) != (expected_sha256 is None):
raise M49PortableResultError(f"{label} expected identity is incomplete")
if expected_byte_length is not None and (
byte_length != expected_byte_length or expected_byte_length < 1
):
raise M49PortableResultError(f"{label} byte length changed")
if byte_length % 16:
raise M49PortableResultError(f"{label} byte shape changed") raise M49PortableResultError(f"{label} byte shape changed")
values = np.fromfile(candidate, dtype="<f4") if expected_sha256 is None:
values = np.fromfile(candidate, dtype="<f4")
else:
payload = candidate.read_bytes()
if hashlib.sha256(payload).hexdigest() != expected_sha256:
raise M49PortableResultError(f"{label} digest changed")
values = np.frombuffer(payload, dtype="<f4")
result = values.reshape(-1, 4) result = values.reshape(-1, 4)
if not np.isfinite(result).all(): if not np.isfinite(result).all():
raise M49PortableResultError(f"{label} contains non-finite points") raise M49PortableResultError(f"{label} contains non-finite points")
@@ -346,6 +346,68 @@ def validate_m49_portable_source_stage(root: Path) -> M49PortableSourceStage:
) )
def validate_m49_portable_source_stage_binding(
stage: M49PortableSourceStage,
) -> M49PortableSourceStage:
"""Rebind an admitted stage without re-hashing its large point files.
A fresh stage was produced and hashed by this materializer; a reused stage
has already passed ``validate_m49_portable_source_stage``. The runner only
needs to prove that the bound root and its small canonical manifest still
name that stage. Result assembly checks each point-file digest while doing
its required read, avoiding a separate multi-gigabyte pass.
"""
resolved = _safe_directory(stage.root, "bound portable source stage")
payload, manifest = _read_canonical_document(
resolved / M49_PORTABLE_STAGE_MANIFEST,
expected_sha256=stage.manifest_sha256,
label="bound M4.9 source stage manifest",
)
identity = _object(manifest.get("identity"), "bound M4.9 source stage identity")
identity_sha256 = _string(
manifest.get("identity_sha256"),
"bound M4.9 source stage identity sha256",
)
timeline = _object(identity.get("timeline"), "bound M4.9 source stage timeline")
frame_count = _positive_int(timeline.get("frame_count"), "timeline frame count")
available_count = _positive_int(
timeline.get("available_lidar_frame_count"),
"available LiDAR frame count",
)
artifacts = _object(manifest.get("artifacts"), "bound M4.9 source stage artifacts")
if set(artifacts) != {"schedule", "sequence-index"}:
raise M49PortableSourceError("bound M4.9 source stage artifacts changed")
schedule = _verify_stage_artifact(resolved, artifacts["schedule"], "schedule")
index = _verify_stage_artifact(
resolved,
artifacts["sequence-index"],
"sequence-index",
)
records = read_m49_source_index(index, expected_frame_count=frame_count)
_verify_schedule(schedule, records)
if (
manifest.get("schema_version") != M49_PORTABLE_SOURCE_STAGE_SCHEMA
or manifest.get("authority") != _AUTHORITY
or identity.get("authority") != _AUTHORITY
or hashlib.sha256(canonical_json(identity)).hexdigest() != identity_sha256
or identity_sha256 != stage.identity_sha256
or hashlib.sha256(payload).hexdigest() != stage.manifest_sha256
or resolved.name != f"{M49_PORTABLE_STAGE_PREFIX}{identity_sha256}"
or frame_count != stage.timeline_frame_count
or available_count != stage.available_lidar_frame_count
or sum(bool(row["sample_available"]) for row in records) != available_count
):
raise M49PortableSourceError("bound portable source stage changed")
return M49PortableSourceStage(
root=resolved,
identity_sha256=identity_sha256,
manifest_sha256=stage.manifest_sha256,
timeline_frame_count=frame_count,
available_lidar_frame_count=available_count,
)
def read_m49_source_index( def read_m49_source_index(
path: Path, path: Path,
*, *,
+54 -13
View File
@@ -31,6 +31,7 @@ from k1link.observatory.m49_portable_source import (
materialize_m49_portable_source, materialize_m49_portable_source,
read_m49_source_index, read_m49_source_index,
validate_m49_portable_source_stage, validate_m49_portable_source_stage,
validate_m49_portable_source_stage_binding,
) )
from k1link.observatory.portable_result_contract import ( from k1link.observatory.portable_result_contract import (
PortableResultPackageManifest, PortableResultPackageManifest,
@@ -566,6 +567,36 @@ def test_fresh_source_stage_defers_full_validation_until_execution_boundary(
assert validated_roots == [first_root] assert validated_roots == [first_root]
def test_bound_source_stage_rechecks_small_documents_without_rehashing_sequence(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
stage_root, _expected = _materialized_source(tmp_path, monkeypatch)
stage = validate_m49_portable_source_stage(stage_root)
def unexpected_sequence_rehash(*_args: object, **_kwargs: object) -> None:
raise AssertionError("bound-stage validation rehashed the point sequence")
monkeypatch.setattr(source_module, "_verify_sequence_files", unexpected_sequence_rehash)
rebound = validate_m49_portable_source_stage_binding(stage)
assert rebound == stage
@pytest.mark.parametrize("relative_path", ["schedule.tsv", "sequence-index.ndjson"])
def test_bound_source_stage_rejects_changed_small_documents(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
relative_path: str,
) -> None:
stage_root, _expected = _materialized_source(tmp_path, monkeypatch)
stage = validate_m49_portable_source_stage(stage_root)
artifact = stage.root / relative_path
artifact.write_bytes(artifact.read_bytes() + b"changed")
with pytest.raises(M49PortableSourceError):
validate_m49_portable_source_stage_binding(stage)
def test_source_materializer_rejects_unadmitted_adjacent_metadata( def test_source_materializer_rejects_unadmitted_adjacent_metadata(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
@@ -756,20 +787,18 @@ def test_result_v2_assembler_and_exact_validator_round_trip(
created_at_utc=lambda: NOW, created_at_utc=lambda: NOW,
invoker=fake_invoke, invoker=fake_invoke,
) )
draft = runner.run( plan = PortableWorkerRuntimePlan(
PortableWorkerRuntimePlan( job_id=sealed.job_id,
job_id=sealed.job_id, adapter_id="m49-tgs-worker006-portable-v2",
adapter_id="m49-tgs-worker006-portable-v2", candidate_sha256="f" * 64,
candidate_sha256="f" * 64, setup_id=sealed.setup_id,
setup_id=sealed.setup_id, definition_sha256=sealed.definition_sha256,
definition_sha256=sealed.definition_sha256, source_bundle_sha256=sealed.source_bundle_sha256,
source_bundle_sha256=sealed.source_bundle_sha256, source_capability_manifest_sha256=sealed.source_capability_manifest_sha256,
source_capability_manifest_sha256=sealed.source_capability_manifest_sha256, result_contract_sha256=definition.result_contract.contract_sha256,
result_contract_sha256=definition.result_contract.contract_sha256, phases=M49_PORTABLE_RUNTIME_PHASES,
phases=M49_PORTABLE_RUNTIME_PHASES,
),
bound_source,
) )
draft = runner.run(plan, bound_source)
package = PortableResultPackageManifest.from_bytes((draft.root / "manifest.json").read_bytes()) package = PortableResultPackageManifest.from_bytes((draft.root / "manifest.json").read_bytes())
assert draft.root.name == package.manifest_sha256 assert draft.root.name == package.manifest_sha256
assert draft.result_id.startswith("m49-tgs-portable-review-") assert draft.result_id.startswith("m49-tgs-portable-review-")
@@ -816,6 +845,18 @@ def test_result_v2_assembler_and_exact_validator_round_trip(
) )
) )
source_rows = read_m49_source_index(
source_stage.root / "sequence-index.ndjson",
expected_frame_count=source_stage.timeline_frame_count,
)
available_row = next(row for row in source_rows if row["sample_available"] is True)
native_path = source_stage.root / cast(str, available_row["relative_path"])
native_payload = bytearray(native_path.read_bytes())
native_payload[-1] ^= 0x01
native_path.write_bytes(native_payload)
with pytest.raises(M49PortableResultError, match="digest changed"):
runner.run(plan, bound_source)
def test_executor_release_candidate_is_deterministic_blocked_and_tamper_evident( def test_executor_release_candidate_is_deterministic_blocked_and_tamper_evident(
tmp_path: Path, tmp_path: Path,