From 333e94b03868db8687979020b49c7776837cb60a Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 11 Oct 2026 15:44:56 +0800 Subject: [PATCH] refactor: move experiment workers out of the node runtime --- AGENTS.md | 18 +- ARCHITECTURE.md | 19 +- README.md | 28 +- cmd/README.md | 6 +- docs/evaluation-local-files.md | 41 - docs/go-api.md | 4 +- docs/runtime-migration.md | 2 +- docs/v0.0.5-parity.md | 2 +- go.mod | 4 - go.sum | 24 - internal/architecture/dependencies_test.go | 31 +- internal/cas/cas.go | 7 +- .../evaluation/authenticationgraph/graph.go | 404 ------- .../authenticationgraph/graph_test.go | 218 ---- internal/evaluation/e0selftest/e0selftest.go | 614 ---------- .../evaluation/e0selftest/e0selftest_test.go | 116 -- .../evaluation/gatewaytransport/accounting.go | 52 - .../gatewaytransport/authentication.go | 206 ---- .../gatewaytransport/authentication_test.go | 161 --- .../evaluation/gatewaytransport/client.go | 562 --------- .../gatewaytransport/client_test.go | 406 ------- .../gatewaytransport/flatmap_test.go | 101 -- .../evaluation/gatewaytransport/flatprefix.go | 169 --- .../machine/machine-descriptor.schema.json | 31 - internal/evaluation/machine/machine.go | 250 ---- internal/evaluation/machine/machine_test.go | 62 - internal/evaluation/machine/probe_linux.go | 102 -- internal/evaluation/machine/probe_other.go | 9 - internal/evaluation/rq2e0/file.go | 70 -- internal/evaluation/rq2e0/fixture.go | 70 -- internal/evaluation/rq2e0/gateway.go | 230 ---- internal/evaluation/rq2e0/harness.go | 163 --- .../evaluation/rq2fixture/authentication.go | 100 -- internal/evaluation/rq2fixture/fixture.go | 617 ---------- .../evaluation/rq2fixture/fixture_test.go | 101 -- internal/evaluation/rq2fixture/oracle.go | 137 --- .../rq2fixture/source-definition.schema.json | 73 -- internal/evaluation/rq2fixture/source.go | 183 --- .../rq2fixture/source_schema_test.go | 137 --- internal/evaluation/rq2metrics/contract.go | 180 --- .../evaluation/rq2metrics/contract_test.go | 125 -- internal/evaluation/rq2wire/wire.go | 465 -------- internal/evaluation/rq2wire/wire_test.go | 26 - internal/evaluation/rq2write/project.go | 166 --- internal/evaluation/rq2write/project_test.go | 148 --- .../evaluation/rq3baseline/archive_index.go | 44 - .../evaluation/rq3baseline/archive_store.go | 461 -------- .../rq3baseline/archive_store_test.go | 324 ----- .../evaluation/rq3baseline/payload_store.go | 150 --- internal/evaluation/rq3baseline/readback.go | 267 ----- .../evaluation/rq3baseline/readback_test.go | 140 --- internal/evaluation/rq3baseline/run.go | 247 ---- internal/evaluation/rq3baseline/run_test.go | 629 ---------- internal/evaluation/rq3baseline/store.go | 319 ----- internal/evaluation/rq3baseline/stream.go | 318 ----- internal/evaluation/rq3baseline/types.go | 304 ----- internal/evaluation/rq3baseline/validate.go | 673 ----------- nodeapi/block.go | 21 + nodeapi/block_test.go | 31 + tools/evaluation/README.md | 46 - .../cmd/malt-eval-local-files/main.go | 339 ------ .../cmd/malt-eval-local-files/main_test.go | 99 -- .../main.go | 47 - .../cmd/malt-eval-replay-worker/main.go | 199 ---- .../cmd/malt-eval-replay-worker/main_test.go | 123 -- .../cmd/malt-eval-rooted-file-trace/main.go | 231 ---- .../malt-eval-rooted-file-trace/main_test.go | 41 - .../cmd/malt-eval-rq1-fixture-build/main.go | 540 --------- .../malt-eval-rq1-fixture-build/main_test.go | 91 -- .../cmd/malt-eval-rq1-worker/main.go | 667 ----------- .../cmd/malt-eval-rq1-worker/main_test.go | 171 --- .../cmd/malt-eval-rq1-worker/rss_bytes.go | 5 - .../cmd/malt-eval-rq1-worker/rss_kib.go | 5 - .../cmd/malt-eval-rq1-worker/selftest.go | 452 ------- .../cmd/malt-eval-rq1-worker/selftest_test.go | 138 --- .../cmd/malt-eval-rq1-worker/usage_other.go | 15 - .../cmd/malt-eval-rq1-worker/usage_unix.go | 34 - .../cmd/malt-eval-rq2-browser-wasm/main.go | 499 -------- .../main_unsupported.go | 9 - .../malt-eval-rq2-browser-worker/artifacts.go | 134 --- .../malt-eval-rq2-browser-worker/browser.go | 669 ----------- .../cmd/malt-eval-rq2-browser-worker/cdp.go | 361 ------ .../malt-eval-rq2-browser-worker/cdp_test.go | 42 - .../cmd/malt-eval-rq2-browser-worker/main.go | 571 --------- .../malt-eval-rq2-browser-worker/main_test.go | 480 -------- .../process_linux.go | 24 - .../process_other.go | 14 - .../resident_linux.go | 111 -- .../resident_other.go | 15 - .../malt-eval-rq2-browser-worker/selftest.go | 888 -------------- .../selftest_test.go | 261 ----- .../cmd/malt-eval-rq2-fixture-build/main.go | 423 ------- .../malt-eval-rq2-fixture-build/main_test.go | 134 --- .../cmd/malt-eval-rq2-worker/main.go | 407 ------- .../cmd/malt-eval-rq2-worker/main_test.go | 93 -- .../cmd/malt-eval-rq2-worker/native.go | 310 ----- .../cmd/malt-eval-rq2-worker/planner.go | 312 ----- .../cmd/malt-eval-rq2-worker/planner_test.go | 126 -- .../cmd/malt-eval-rq2-worker/rss_bytes.go | 5 - .../cmd/malt-eval-rq2-worker/rss_kib.go | 5 - .../cmd/malt-eval-rq2-worker/selftest.go | 501 -------- .../cmd/malt-eval-rq2-worker/selftest_test.go | 211 ---- .../cmd/malt-eval-rq2-worker/usage_other.go | 14 - .../cmd/malt-eval-rq2-worker/usage_unix.go | 32 - .../cmd/malt-eval-rq3-hash-worker/main.go | 346 ------ .../malt-eval-rq3-hash-worker/main_test.go | 144 --- .../cmd/malt-eval-rq3-hash-worker/selftest.go | 468 -------- .../malt-eval-rq3-malt-worker/accounting.go | 343 ------ .../cmd/malt-eval-rq3-malt-worker/direct.go | 614 ---------- .../cmd/malt-eval-rq3-malt-worker/flat.go | 109 -- .../flat_oracle_test.go | 54 - .../cmd/malt-eval-rq3-malt-worker/main.go | 315 ----- .../malt-eval-rq3-malt-worker/main_test.go | 746 ------------ .../cmd/malt-eval-rq3-malt-worker/model.go | 243 ---- .../payload_store.go | 179 --- .../malt-eval-rq3-malt-worker/role_index.go | 116 -- .../role_index_test.go | 105 -- .../cmd/malt-eval-rq3-malt-worker/selftest.go | 1037 ----------------- .../selftest_directory_linux.go | 96 -- .../selftest_directory_linux_test.go | 38 - .../selftest_directory_unsupported.go | 19 - .../cmd/malt-eval-rq3-malt-worker/types.go | 204 ---- .../cmd/malt-eval-rq3-malt-worker/worker.go | 268 ----- .../worker_health_test.go | 84 -- 124 files changed, 96 insertions(+), 25894 deletions(-) delete mode 100644 docs/evaluation-local-files.md delete mode 100644 internal/evaluation/authenticationgraph/graph.go delete mode 100644 internal/evaluation/authenticationgraph/graph_test.go delete mode 100644 internal/evaluation/e0selftest/e0selftest.go delete mode 100644 internal/evaluation/e0selftest/e0selftest_test.go delete mode 100644 internal/evaluation/gatewaytransport/accounting.go delete mode 100644 internal/evaluation/gatewaytransport/authentication.go delete mode 100644 internal/evaluation/gatewaytransport/authentication_test.go delete mode 100644 internal/evaluation/gatewaytransport/client.go delete mode 100644 internal/evaluation/gatewaytransport/client_test.go delete mode 100644 internal/evaluation/gatewaytransport/flatmap_test.go delete mode 100644 internal/evaluation/gatewaytransport/flatprefix.go delete mode 100644 internal/evaluation/machine/machine-descriptor.schema.json delete mode 100644 internal/evaluation/machine/machine.go delete mode 100644 internal/evaluation/machine/machine_test.go delete mode 100644 internal/evaluation/machine/probe_linux.go delete mode 100644 internal/evaluation/machine/probe_other.go delete mode 100644 internal/evaluation/rq2e0/file.go delete mode 100644 internal/evaluation/rq2e0/fixture.go delete mode 100644 internal/evaluation/rq2e0/gateway.go delete mode 100644 internal/evaluation/rq2e0/harness.go delete mode 100644 internal/evaluation/rq2fixture/authentication.go delete mode 100644 internal/evaluation/rq2fixture/fixture.go delete mode 100644 internal/evaluation/rq2fixture/fixture_test.go delete mode 100644 internal/evaluation/rq2fixture/oracle.go delete mode 100644 internal/evaluation/rq2fixture/source-definition.schema.json delete mode 100644 internal/evaluation/rq2fixture/source.go delete mode 100644 internal/evaluation/rq2fixture/source_schema_test.go delete mode 100644 internal/evaluation/rq2metrics/contract.go delete mode 100644 internal/evaluation/rq2metrics/contract_test.go delete mode 100644 internal/evaluation/rq2wire/wire.go delete mode 100644 internal/evaluation/rq2wire/wire_test.go delete mode 100644 internal/evaluation/rq2write/project.go delete mode 100644 internal/evaluation/rq2write/project_test.go delete mode 100644 internal/evaluation/rq3baseline/archive_index.go delete mode 100644 internal/evaluation/rq3baseline/archive_store.go delete mode 100644 internal/evaluation/rq3baseline/archive_store_test.go delete mode 100644 internal/evaluation/rq3baseline/payload_store.go delete mode 100644 internal/evaluation/rq3baseline/readback.go delete mode 100644 internal/evaluation/rq3baseline/readback_test.go delete mode 100644 internal/evaluation/rq3baseline/run.go delete mode 100644 internal/evaluation/rq3baseline/run_test.go delete mode 100644 internal/evaluation/rq3baseline/store.go delete mode 100644 internal/evaluation/rq3baseline/stream.go delete mode 100644 internal/evaluation/rq3baseline/types.go delete mode 100644 internal/evaluation/rq3baseline/validate.go create mode 100644 nodeapi/block.go create mode 100644 nodeapi/block_test.go delete mode 100644 tools/evaluation/README.md delete mode 100644 tools/evaluation/cmd/malt-eval-local-files/main.go delete mode 100644 tools/evaluation/cmd/malt-eval-local-files/main_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-machine-descriptor-probe/main.go delete mode 100644 tools/evaluation/cmd/malt-eval-replay-worker/main.go delete mode 100644 tools/evaluation/cmd/malt-eval-replay-worker/main_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rooted-file-trace/main.go delete mode 100644 tools/evaluation/cmd/malt-eval-rooted-file-trace/main_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq1-fixture-build/main.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq1-fixture-build/main_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq1-worker/main.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq1-worker/main_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq1-worker/rss_bytes.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq1-worker/rss_kib.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq1-worker/selftest.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq1-worker/selftest_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq1-worker/usage_other.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq1-worker/usage_unix.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-browser-wasm/main.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-browser-wasm/main_unsupported.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-browser-worker/artifacts.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-browser-worker/browser.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-browser-worker/cdp.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-browser-worker/cdp_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-browser-worker/main.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-browser-worker/main_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-browser-worker/process_linux.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-browser-worker/process_other.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-browser-worker/resident_linux.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-browser-worker/resident_other.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-browser-worker/selftest.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-browser-worker/selftest_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-fixture-build/main.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-fixture-build/main_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-worker/main.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-worker/main_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-worker/native.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-worker/planner.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-worker/planner_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-worker/rss_bytes.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-worker/rss_kib.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-worker/selftest.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-worker/selftest_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-worker/usage_other.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq2-worker/usage_unix.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-hash-worker/main.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-hash-worker/main_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-hash-worker/selftest.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/accounting.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/direct.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/flat.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/flat_oracle_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/main.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/main_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/model.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/payload_store.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/role_index.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/role_index_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/selftest.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/selftest_directory_linux.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/selftest_directory_linux_test.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/selftest_directory_unsupported.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/types.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/worker.go delete mode 100644 tools/evaluation/cmd/malt-eval-rq3-malt-worker/worker_health_test.go diff --git a/AGENTS.md b/AGENTS.md index 790f582..9c42a55 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,8 +5,9 @@ This repository owns the user-controlled MALT local data runtime: CLI, local daemon, accepted-root policy, UnixFS application semantics, Merkle DAG import compatibility, pluggable transport capabilities, and payload-byte verification. -It also keeps the runtime-owned executable adapters needed to measure those exact behaviors; -the evaluator that plans and interprets campaigns lives in `malt-evaluation`. +This is the edge node implementation. Reproducible measurement drivers, replay +workers, experiment protocols, and accounting policy live in `malt-evaluation` +and consume public node/runtime capabilities. ## Boundaries @@ -28,9 +29,9 @@ the evaluator that plans and interprets campaigns lives in `malt-evaluation`. - Keep IPFS-style Merkle DAG import and compatibility policy here. A Merkle DAG root is a compatibility output, not a MALT-authenticated root or ProofList. - Keep benchmark suites, run plans, comparison policy, result schemas, and - result provenance in `malt-evaluation`. Code under `tools/evaluation` is a - pinned process adapter, not a supported runtime command or an evaluator - source of truth. + result provenance in `malt-evaluation`. Do not place experiment workers or + experiment-specific accounting in this node repository. Expose only reusable + node/runtime capabilities and storage observation needed by external callers. ## Package Ownership @@ -163,13 +164,6 @@ the evaluator that plans and interprets campaigns lives in `malt-evaluation`. select configured runtime services, call `application/` use cases, and format results, not become a second UnixFS, trust, or plan-composition implementation. `cmd/` contains only the supported `malt` product binary. -- `tools/evaluation/cmd/` owns evaluator-launched runtime process adapters. - Preserve their external binary names and wire contracts, but do not treat - their Go packages as a compatibility surface. -- `internal/evaluation/` owns shared implementation for those adapters, - including disposable-Gateway instance authentication, bootstrap, raw-CAS, - and selective-CAR transport. Production packages and `cmd/malt` must not - import it. - Preserve `internal/architecture` dependency tests when adding packages or moving responsibilities. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9bedd25..6b8ada7 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -19,10 +19,11 @@ It owns: - application-level payload-byte verification. - runtime-side encrypted snapshot creation, restore, and automatic scheduling. -Runtime-specific benchmark process adapters also live in this repository under -`tools/evaluation`. They exercise this implementation at a pinned commit, but -do not own benchmark plans, suites, comparison policy, result schemas, or -result provenance; those belong in `malt-evaluation`. +Benchmark process adapters, replay workers, experiment fixtures, accounting, +plans, suites and provenance belong in `malt-evaluation`. Its adapters exercise +this node through public capabilities at a pinned module version. This runtime +and Gateway provide edge and cloud node implementations; measurements remain +outside the node implementation. It depends on `github.com/dewebprotocol/malt-core` for canonical graph types, typed query/candidate/batch contracts, local verification, CID rules, and @@ -124,7 +125,7 @@ merkledag -> CID/link replay + fixed profile transport trust -> accepted/candidate root persistence transport -> Gateway/local/hybrid adapters; never imports unixfs, merkledag, or trust bucketsync -> nodeapi.DatasetBranch + independent durable synchronization metadata -tools/evaluation/cmd -> internal/evaluation + public runtime capabilities +malt-evaluation adapters -> public nodeapi / runtime capabilities (external dependency) ``` `transport.Client` is one reusable HTTP connection, but consumers depend on @@ -134,8 +135,8 @@ the fixed `PostMerkleDAGResolve` and `PostMerkleDAGRead` capabilities; there is no arbitrary profile-route escape hatch. Transport results remain untrusted. The public transport also does not expose evaluation instance credentials, bootstrap control, unchecked raw-CAS reads, or the selective-CAR route. Those -disposable-Gateway capabilities live under `internal/evaluation` and can only -be composed by evaluator adapters. +disposable-Gateway capabilities live under `malt-evaluation/internal/nodeeval` +and can only be composed by evaluator adapters. `transport/local` is a bounded, atomic, owner-private durable CAS and verifies the complete body on both `Get` and `Has`. `transport/hybrid` is transport-level @@ -287,8 +288,6 @@ profile; it does not change MALT Core or the existing plaintext semantics of ## Packages - `cmd/malt`: CLI and daemon process lifecycle. -- `tools/evaluation/cmd`: evaluator-launched process adapters. Their executable - names and wire contracts are campaign inputs, not supported user CLI. - `application`: reusable trusted-root, UnixFS, and Merkle DAG use cases shared by command and daemon adapters. - `application/backup`: encrypted MALT-native filesystem, local fingerprint, Bucket @@ -394,8 +393,6 @@ profile; it does not change MALT Core or the existing plaintext semantics of - `internal/securefile`: owner-only Unix file modes and protected owner-and-SYSTEM Windows file DACLs. - `internal/cas`: runtime-side CAS helpers and byte verification. -- `internal/evaluation`: private adapter support, including evaluation Gateway - authentication/control transport and shared workload machinery. - `unixfs/model`: UnixFS application values and path rules. - `unixfs`: verified UnixFS reader/writer facade, staging, materialization, and payload verification. diff --git a/README.md b/README.md index af8739c..de74514 100644 --- a/README.md +++ b/README.md @@ -532,20 +532,17 @@ and cannot fall back to the public CAS namespace. ## Evaluation adapters -`cmd/` contains only the supported `malt` product binary. Runtime-specific -benchmark process adapters live under `tools/evaluation/cmd`, with shared -private support under `internal/evaluation`. They preserve their external -`malt-eval-*` executable and wire contracts, but are not a public Go API or -supported user CLI. - -Evaluation instance authentication, bootstrap control, unchecked raw-CAS -fetches, and selective-CAR transport are deliberately confined to that private -boundary. The public `transport` package cannot invoke them. Benchmark suites, -campaign plans, comparison policy, result schemas, and provenance remain owned -by the separate -[malt-evaluation](https://github.com/DeWebProtocol/malt-evaluation) repository. -See [tools/evaluation/README.md](./tools/evaluation/README.md) for the adapter -inventory and build boundary. +`cmd/` contains only the supported `malt` product binary. Replay workers, +benchmark drivers, experimental transports, fixtures, accounting and result +schemas live in the separate +[malt-evaluation](https://github.com/DeWebProtocol/malt-evaluation) repository, +under `adapters/node/cmd` and `internal/nodeeval`. + +The evaluator calls the public `nodeapi`, UnixFS, Merkle DAG and transport +capabilities. The node provides storage and authentication behavior; the +evaluator defines workloads and measurement boundaries. Gateway and this +runtime are cloud and edge implementations of node capabilities, respectively. +Evaluation builds pin the node module separately from the evaluator revision. The transport exposes bounded ordered CAS `PutBatch`/`HasBatch` and a typed diagnostic metrics snapshot. Merkle DAG import and verified link replay @@ -627,7 +624,8 @@ for completion. None of these operations accepts a candidate automatically. The candidate API is a complete-view workflow, not a stateless transition proof. Evaluation tooling can export bounded file snapshots with -`go run ./tools/evaluation/cmd/malt-eval-rooted-file-trace --input snapshots.json`. +`go run ./adapters/node/cmd/malt-eval-rooted-file-trace --input snapshots.json` +from a `malt-evaluation` checkout. The input is an array of complete versions, each containing `files` (canonical relative paths mapped to base64 bytes) and `queries` (canonical paths). `--backend ipa` and `--chunk-size` select the commitment and file chunk geometry. diff --git a/cmd/README.md b/cmd/README.md index c224676..5c2f9c4 100644 --- a/cmd/README.md +++ b/cmd/README.md @@ -6,6 +6,6 @@ This namespace contains only the supported `malt` runtime binary: go build -o bin/malt ./cmd/malt ``` -Benchmark workers are not product commands. They live under -[`tools/evaluation/cmd`](../tools/evaluation/cmd) so packaging and API reviews -cannot confuse evaluator process adapters with the user-facing CLI. +Benchmark workers are not product commands. They live in +[`malt-evaluation/adapters/node/cmd`](https://github.com/DeWebProtocol/malt-evaluation/tree/main/adapters/node/cmd) +and call public node/runtime capabilities. diff --git a/docs/evaluation-local-files.md b/docs/evaluation-local-files.md deleted file mode 100644 index bb7047e..0000000 --- a/docs/evaluation-local-files.md +++ /dev/null @@ -1,41 +0,0 @@ -# Verified local-file evaluation leaf - -`tools/evaluation/cmd/malt-eval-local-files` exercises the actual rooted-v1 -UnixFS writer, payload CAS and verified client reader. It accepts one bounded -file fixture and reuses it for full reads, byte ranges and authenticated missing -paths. The process adapter is evaluator tooling, not a supported product command. -The request/result contract is maintained by `malt-evaluation` in -`docs/local-files-worker-v1.md`. - -The fixture contains canonical regular-file paths and actual source bytes. -Every chunk and directory manifest is stored in a real in-memory CAS, while -authentication candidates are materialized through the public Core SDK. -Initialization reads every source file through the verified UnixFS facade and -compares its bytes with the independent fixture before measured queries begin. - -Reads start from the caller-selected entry root and original path. The client -fetches, decodes and verifies serialized authentication results, then binds each -required CAS body to authenticated CIDs. Ranges include actual Positional -metadata and chunk verification, including partial terminal chunks. A missing -path succeeds only after the reader's locally verified absence result; corrupt -CAS bytes or missing backend objects cannot be classified as authenticated -absence. The process never promotes a root into the runtime trust store. - -Counters describe actual calls to the authentication and payload capabilities. -Proof bytes count serialized `AuthenticationResult` envelopes; CAS bytes count -all returned block bodies, including manifests and repeated accesses. They are -not network wire bytes, persistent writes or internal materializer lookups. -The response separately identifies returned range/file length and its digest. -Fixture construction and complete readback remain outside read latency. - -The fixture is bounded by 32 MiB of source payload and 100,000 regular files; -chunks are between one byte and 4 MiB. The JSONL line bound is 64 MiB. Canonical -paths, collisions, operation-specific fields, duplicate keys/IDs, response -identity and explicit terminal close are checked. stdout is JSONL only. -The fixture implements no modes, symlinks, mutations, durability, HTTP, CAR, -cold-reset, checkpoint or resource-budget capability. - -Runtime tests cover KZG/IPA full reads, empty files, real side objects, shallow -and deep missing paths, ranges spanning chunks, partial terminal chunks, changed -entry roots, corrupted bodies and invalid protocol/lifecycle input. Linux -full-module test/vet/build and whitespace validation passed for this leaf. diff --git a/docs/go-api.md b/docs/go-api.md index 61f0488..9b8b743 100644 --- a/docs/go-api.md +++ b/docs/go-api.md @@ -185,8 +185,8 @@ defines no network codec, discovery rule, or wire identifier. The public transport does not expose evaluation instance tokens, bootstrap control, unchecked raw-CAS reads, or the selective-CAR route. Those -capabilities are private to the pinned process adapters under -`tools/evaluation`; they are not a supported integration surface. +capabilities are private to the process adapters in the separate +`malt-evaluation` repository; they are not a supported node integration surface. `Metrics` returns inexpensive monotonic counters. `MetricsWithStorage` also requests Gateway's O(live KV entries) logical scan and should be used only by diff --git a/docs/runtime-migration.md b/docs/runtime-migration.md index 00e8b55..df43175 100644 --- a/docs/runtime-migration.md +++ b/docs/runtime-migration.md @@ -19,7 +19,7 @@ not a current build contract. The original plan is available in | `internal/runtime` | Reusable composition of local services and transports | | `cmd/malt` | CLI and daemon entrypoints | | `merkledag` | Isolated IPFS-compatible import and CID/link replay | -| `tools/evaluation` | Private measured workers, separate from production policy | +| Evaluation workers | Moved to `malt-evaluation/adapters/node/cmd`; consume public node/runtime capabilities | The runtime's Go module namespace remains `github.com/dewebprotocol/malt-client` until its separately governed module migration. The module pins published Core diff --git a/docs/v0.0.5-parity.md b/docs/v0.0.5-parity.md index d4e1296..0d56174 100644 --- a/docs/v0.0.5-parity.md +++ b/docs/v0.0.5-parity.md @@ -20,7 +20,7 @@ repository at v0.0.5. | ArcTable, KV, and CAS persistence | gateway | migrated; not imported by runtime or core | | standalone CAS process | none | deliberately removed; gateway owns product CAS | | evaluator and benchmark suites | [malt-evaluation](https://github.com/DeWebProtocol/malt-evaluation) | former v0.0.5 evaluator retired from the active tree and recoverable from the [immutable standalone import commit](https://github.com/DeWebProtocol/malt-evaluation/commit/774854b5d90c1ba5d57c3d74619dd97a78fd9dcf) and Git history; active workloads live at the repository root | -| current runtime measurement adapters | malt | evaluator-launched `malt-eval-*` process boundaries live under `tools/evaluation/cmd`; shared implementation is private under `internal/evaluation`, while plans, suites, schemas, and provenance remain in `malt-evaluation` | +| current runtime measurement adapters | malt-evaluation | `adapters/node/cmd` and `internal/nodeeval` own workers and experimental support; the runtime exports only public node/application capabilities | | Merkle DAG gateway compatibility reads | gateway + malt | gateway traverses UnixFS and returns CID-bound blocks; the runtime independently hashes and replays them under a distinct non-MALT profile | | profile-specific transport escape hatch | none | arbitrary route/body transport was removed; public Merkle DAG compatibility uses fixed resolve/read capabilities, while evaluation-only bootstrap/raw-CAS/selective-CAR routes are private adapter implementation | | generic IPLD `ParseBlock`/link helpers | malt | retired: unused generic parser removed; supported import and verified DAG-PB/raw/DAG-CBOR/DAG-JSON replay remain in `merkledag` | diff --git a/go.mod b/go.mod index 5808027..5af3045 100644 --- a/go.mod +++ b/go.mod @@ -15,9 +15,7 @@ require ( github.com/ipld/go-ipld-prime v0.24.0 github.com/multiformats/go-multibase v0.3.0 github.com/multiformats/go-multihash v0.2.3 - github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/spf13/cobra v1.10.2 - github.com/syndtr/goleveldb v1.0.0 golang.org/x/crypto v0.57.0 golang.org/x/sys v0.48.0 golang.org/x/term v0.46.0 @@ -33,7 +31,6 @@ require ( github.com/gammazero/deque v1.2.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -71,7 +68,6 @@ require ( go.uber.org/zap v1.28.0 // indirect golang.org/x/exp v0.0.0-20260603202125-055de637280b // indirect golang.org/x/sync v0.23.0 // indirect - golang.org/x/text v0.42.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/protobuf v1.36.11 // indirect lukechampine.com/blake3 v1.4.1 // indirect diff --git a/go.sum b/go.sum index 5bd3ba8..17b459c 100644 --- a/go.sum +++ b/go.sum @@ -25,13 +25,10 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3h github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/dewebprotocol/malt-core v0.0.11-rc.1 h1:V1PC7myStsHDeAjy6DkNwkOGyWlJF4vXYFQSwgAbb4A= github.com/dewebprotocol/malt-core v0.0.11-rc.1/go.mod h1:Ewf3s5XYFlgsX2PhcOKY2BLqv9jcYXYEFaYhDHjO/Y0= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU= github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gammazero/chanqueue v1.1.2 h1:dZEsxlyANZMyeTRemABqZF8QM9BnE4NBI43Oh3y5fIU= github.com/gammazero/chanqueue v1.1.2/go.mod h1:XDN1X/jjAbmSceNFOQbtKToeSkxtdVdpKu90LiEdBEE= github.com/gammazero/deque v1.2.1 h1:9fnQVFCCZ9/NOc7ccTNqzoKd1tCWOqeI05/lPqFPMGQ= @@ -41,7 +38,6 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -54,8 +50,6 @@ github.com/hanwen/go-fuse/v2 v2.11.0 h1:CGVkJh9gRz0pTRMADNcqdFl3ec/5QbE/Vx1Gl7ES github.com/hanwen/go-fuse/v2 v2.11.0/go.mod h1:aU7NkGYZUmuJrZapoI3mEcNve7PZTySUOLBuch/vR6U= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -170,11 +164,6 @@ github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOo github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 h1:1/WtZae0yGtPq+TI6+Tv1WTxkukpXeMlviSxvL7SRgk= github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9/go.mod h1:x3N5drFsm2uilKKuuYo6LdyD8vZAW55sH/9w+pbo1sw= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -191,8 +180,6 @@ github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4Ul github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= @@ -247,20 +234,14 @@ golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q= golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= -golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= @@ -268,11 +249,6 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6f google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/architecture/dependencies_test.go b/internal/architecture/dependencies_test.go index 4a5f07b..e2a1226 100644 --- a/internal/architecture/dependencies_test.go +++ b/internal/architecture/dependencies_test.go @@ -62,20 +62,17 @@ func TestRuntimeModuleNamespaceCutoverRemainsExplicit(t *testing.T) { func TestProductionPackagesDoNotImportEvaluation(t *testing.T) { root := moduleRoot(t) + for _, directory := range []string{"tools/evaluation", "internal/evaluation"} { + if _, err := os.Stat(filepath.Join(root, directory)); !os.IsNotExist(err) { + t.Fatalf("node runtime must not contain evaluation drivers at %s: %v", directory, err) + } + } set := token.NewFileSet() err := filepath.WalkDir(root, func(filePath string, entry fs.DirEntry, err error) error { - if err != nil { - return err - } - relative, err := filepath.Rel(root, filePath) if err != nil { return err } if entry.IsDir() { - if relative == filepath.Join("internal", "evaluation") || - relative == filepath.Join("tools", "evaluation") { - return filepath.SkipDir - } return nil } if filepath.Ext(filePath) != ".go" || strings.HasSuffix(filePath, "_test.go") { @@ -90,7 +87,9 @@ func TestProductionPackagesDoNotImportEvaluation(t *testing.T) { if err != nil { return err } - if value == "github.com/dewebprotocol/malt-client/internal/evaluation" || + if value == "github.com/dewebprotocol/malt-evaluation" || + strings.HasPrefix(value, "github.com/dewebprotocol/malt-evaluation/") || + value == "github.com/dewebprotocol/malt-client/internal/evaluation" || strings.HasPrefix(value, "github.com/dewebprotocol/malt-client/internal/evaluation/") { t.Errorf("%s imports forbidden production dependency %s", filePath, value) } @@ -102,7 +101,7 @@ func TestProductionPackagesDoNotImportEvaluation(t *testing.T) { } } -func TestCommandNamespacesSeparateProductAndEvaluationBinaries(t *testing.T) { +func TestCommandNamespaceContainsOnlyProductBinary(t *testing.T) { root := moduleRoot(t) productEntries, err := os.ReadDir(filepath.Join(root, "cmd")) if err != nil { @@ -118,18 +117,6 @@ func TestCommandNamespacesSeparateProductAndEvaluationBinaries(t *testing.T) { t.Fatalf("cmd must contain only the production malt command, got %v", productCommands) } - evaluationEntries, err := os.ReadDir(filepath.Join(root, "tools", "evaluation", "cmd")) - if err != nil { - t.Fatal(err) - } - if len(evaluationEntries) == 0 { - t.Fatal("evaluation command namespace is empty") - } - for _, entry := range evaluationEntries { - if !entry.IsDir() || !strings.HasPrefix(entry.Name(), "malt-eval-") { - t.Errorf("tools/evaluation/cmd contains non-evaluation entry %q", entry.Name()) - } - } } func TestPublicTransportContainsNoEvaluationControlPlane(t *testing.T) { diff --git a/internal/cas/cas.go b/internal/cas/cas.go index 70de145..572781e 100644 --- a/internal/cas/cas.go +++ b/internal/cas/cas.go @@ -7,7 +7,6 @@ import ( "github.com/dewebprotocol/malt-client/nodeapi" cid "github.com/ipfs/go-cid" - mh "github.com/multiformats/go-multihash" ) // ErrNotFound reports that a requested CAS block is absent. Remote and local @@ -78,11 +77,7 @@ func NormalizeCodec(codec uint64) uint64 { // CIDForBlock computes the CID for block bytes and codec. func CIDForBlock(block Block) (cid.Cid, error) { - mhash, err := mh.Sum(block.Data, mh.SHA2_256, -1) - if err != nil { - return cid.Undef, err - } - return cid.NewCidV1(NormalizeCodec(block.Codec), mhash), nil + return nodeapi.CIDForBlock(block) } // PutBlocks stores blocks, using batch writes when supported. diff --git a/internal/evaluation/authenticationgraph/graph.go b/internal/evaluation/authenticationgraph/graph.go deleted file mode 100644 index 82d9017..0000000 --- a/internal/evaluation/authenticationgraph/graph.go +++ /dev/null @@ -1,404 +0,0 @@ -// Package authenticationgraph composes retained Core Writers for bounded, -// disposable-Gateway evaluations. It owns no protocol or accepted-root policy. -package authenticationgraph - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "sync" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-core/engine" - "github.com/dewebprotocol/malt-core/maltcid" - "github.com/dewebprotocol/malt-core/protocol" - "github.com/dewebprotocol/malt-core/sdk/authentication" - cid "github.com/ipfs/go-cid" -) - -const ( - MaxObjects = 4096 - MaxEntries = 65536 - MaxDepth = 256 - MaxBytes = 64 << 20 -) - -type Remote interface { - AuthenticationCandidate(context.Context, cid.Cid) (gatewaytransport.CandidateResponse, error) - SubmitAuthenticationBatch(context.Context, protocol.AuthenticationBatch) (gatewaytransport.BatchResponse, error) -} - -// View contains original typed inputs, without copying complete node vectors. -// It is an independent snapshot; changing it cannot mutate retained Writers. -type View struct { - Root cid.Cid - States map[string]engine.State -} - -func (v View) State(root cid.Cid) (engine.State, error) { - state, ok := v.States[root.String()] - if !ok { - return engine.State{}, fmt.Errorf("authentication graph omits Root %s", root) - } - return state, nil -} - -type object struct { - writer *authentication.Writer - state engine.State - bytes uint64 -} -type graph struct { - root cid.Cid - objects map[string]object -} - -type LoadMetrics struct{ FetchNS, ImportNS, WireBytes, Objects uint64 } -type EditMetrics struct{ ApplyNS, ExportNS, Candidates, CandidateBytes uint64 } -type Result struct { - Base, Root cid.Cid - Receipt protocol.AuthenticationReceipt - Idempotent bool - Computation EditMetrics - Submission gatewaytransport.BatchResponse - SubmitRoundTripNS uint64 -} - -// Session advances only after a receipt binds the exact submitted batch and -// evaluator durable boundary. This is materialization acknowledgement only. -type Session struct { - mu sync.Mutex - remote Remote - engine *engine.Engine - current *graph -} - -func New(remote Remote, e *engine.Engine) (*Session, error) { - if remote == nil || e == nil { - return nil, errors.New("authentication graph needs a remote and engine") - } - return &Session{remote: remote, engine: e}, nil -} -func (s *Session) Load(ctx context.Context, root cid.Cid) (LoadMetrics, error) { - s.mu.Lock() - defer s.mu.Unlock() - next, metrics, err := s.fetch(ctx, root) - if err == nil { - s.current = next - } - return metrics, err -} -func (s *Session) fetch(ctx context.Context, root cid.Cid) (*graph, LoadMetrics, error) { - var metrics LoadMetrics - if err := s.engine.CheckRoot(root); err != nil { - return nil, metrics, err - } - next := &graph{root: root, objects: map[string]object{}} - visiting := map[string]bool{} - heights := map[string]int{} - var entries uint64 - var visit func(cid.Cid, int) error - visit = func(root cid.Cid, depth int) error { - if err := ctx.Err(); err != nil { - return err - } - if depth > MaxDepth { - return errors.New("authentication graph exceeds depth bound") - } - key := root.String() - if visiting[key] { - return errors.New("authentication graph contains a cycle") - } - if _, ok := next.objects[key]; ok { - if depth+heights[key] > MaxDepth { - return errors.New("shared subtree exceeds depth bound") - } - return nil - } - if len(next.objects)+len(visiting) >= MaxObjects { - return errors.New("authentication graph exceeds object bound") - } - visiting[key] = true - started := time.Now() - response, err := s.remote.AuthenticationCandidate(ctx, root) - metrics.FetchNS += elapsed(started) - if err != nil { - return err - } - candidate := response.Candidate - if candidate.Root != key { - return errors.New("candidate response substitutes the selected Root") - } - // Count normalized bytes as well as wire bytes; an in-process Remote is not - // allowed to bypass the same memory bound as the HTTP adapter. - encoded, err := json.Marshal(candidate) - if err != nil { - return err - } - size := max(response.WireBytes, uint64(len(encoded))) - if size > MaxBytes-metrics.WireBytes { - return errors.New("authentication graph exceeds byte bound") - } - metrics.WireBytes += size - if uint64(len(candidate.State.Entries)) > MaxEntries-entries { - return errors.New("authentication graph exceeds entry bound") - } - entries += uint64(len(candidate.State.Entries)) - started = time.Now() - writer, err := authentication.NewWriter(ctx, s.engine, candidate) - metrics.ImportNS += elapsed(started) - if err != nil { - return fmt.Errorf("authenticate candidate %s: %w", key, err) - } - for _, target := range graphTargets(candidate.State) { - if _, _, err := maltcid.ParseRoot(target); err == nil { - if err := visit(target, depth+1); err != nil { - return err - } - heights[key] = max(heights[key], 1+heights[target.String()]) - } - } - next.objects[key] = object{writer: writer, state: cloneState(candidate.State), bytes: size} - delete(visiting, key) - metrics.Objects++ - return nil - } - if err := visit(root, 0); err != nil { - return nil, metrics, err - } - return next, metrics, nil -} -func (s *Session) Snapshot() (View, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.current == nil { - return View{}, errors.New("authentication graph is not loaded") - } - return snapshot(s.current), nil -} -func snapshot(g *graph) View { - v := View{Root: g.root, States: make(map[string]engine.State, len(g.objects))} - for key, object := range g.objects { - v.States[key] = cloneState(object.state) - } - return v -} - -// Audit reimports the durable graph independently, without changing the session. -func (s *Session) Audit(ctx context.Context) error { - s.mu.Lock() - defer s.mu.Unlock() - if s.current == nil { - return errors.New("authentication graph is not loaded") - } - _, _, err := s.fetch(ctx, s.current.root) - return err -} - -// Edit is private local computation against one immutable graph. Callers apply -// children before explicitly replacing the selected parent edge; aliased roots -// therefore retain ordinary copy-on-write behavior. -type Edit struct { - owner *Session - base *graph - pending map[string]object - candidates []protocol.AuthenticationCandidate - metrics EditMetrics -} - -func (s *Session) Begin() (*Edit, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.current == nil { - return nil, errors.New("authentication graph is not loaded") - } - return &Edit{owner: s, base: s.current, pending: map[string]object{}}, nil -} -func (e *Edit) get(root cid.Cid) (object, error) { - if o, ok := e.pending[root.String()]; ok { - return o, nil - } - if o, ok := e.base.objects[root.String()]; ok { - return o, nil - } - return object{}, fmt.Errorf("edit refers to an unavailable Root %s", root) -} -func (e *Edit) State(root cid.Cid) (engine.State, error) { - o, err := e.get(root) - return cloneState(o.state), err -} -func (e *Edit) Metrics() EditMetrics { return e.metrics } -func (e *Edit) Apply(ctx context.Context, root cid.Cid, delta authentication.Delta) (cid.Cid, error) { - o, err := e.get(root) - if err != nil { - return cid.Undef, err - } - started := time.Now() - writer, err := o.writer.Apply(ctx, delta) - e.metrics.ApplyNS += elapsed(started) - if err != nil { - return cid.Undef, err - } - if writer.Root().Equals(root) { - return root, nil - } - return e.retain(ctx, writer) -} -func (e *Edit) Build(ctx context.Context, state engine.State) (cid.Cid, error) { - if len(state.Entries) > MaxEntries { - return cid.Undef, errors.New("new state exceeds entry bound") - } - started := time.Now() - writer, err := authentication.BuildWriter(ctx, e.owner.engine, state) - e.metrics.ApplyNS += elapsed(started) - if err != nil { - return cid.Undef, err - } - return e.retain(ctx, writer) -} -func (e *Edit) retain(ctx context.Context, writer *authentication.Writer) (cid.Cid, error) { - root := writer.Root() - if _, ok := e.pending[root.String()]; ok { - return root, nil - } - if _, ok := e.base.objects[root.String()]; ok { - return root, nil - } - if len(e.pending) >= MaxObjects { - return cid.Undef, errors.New("edit exceeds candidate bound") - } - started := time.Now() - candidate, err := writer.Export(ctx) - e.metrics.ExportNS += elapsed(started) - if err != nil { - return cid.Undef, err - } - data, err := json.Marshal(candidate) - if err != nil { - return cid.Undef, err - } - size := uint64(len(data)) - if size > MaxBytes-e.metrics.CandidateBytes { - return cid.Undef, errors.New("edit exceeds candidate byte bound") - } - e.metrics.CandidateBytes += size - e.metrics.Candidates++ - e.pending[root.String()] = object{writer: writer, state: candidate.State, bytes: size} - e.candidates = append(e.candidates, candidate) - return root, nil -} -func (e *Edit) graph(ctx context.Context, root cid.Cid) (*graph, error) { - next := &graph{root: root, objects: map[string]object{}} - visiting := map[string]bool{} - heights := map[string]int{} - var entries, size uint64 - var visit func(cid.Cid, int) error - visit = func(root cid.Cid, depth int) error { - if err := ctx.Err(); err != nil { - return err - } - if depth > MaxDepth { - return errors.New("new graph exceeds depth bound") - } - key := root.String() - if visiting[key] { - return errors.New("new graph contains a cycle") - } - if _, ok := next.objects[key]; ok { - if depth+heights[key] > MaxDepth { - return errors.New("shared subtree exceeds depth bound") - } - return nil - } - if len(next.objects)+len(visiting) >= MaxObjects { - return errors.New("new graph exceeds object bound") - } - o, err := e.get(root) - if err != nil { - return err - } - if uint64(len(o.state.Entries)) > MaxEntries-entries || o.bytes > MaxBytes-size { - return errors.New("new graph exceeds entry or byte bound") - } - entries += uint64(len(o.state.Entries)) - size += o.bytes - visiting[key] = true - for _, target := range graphTargets(o.state) { - if _, _, err := maltcid.ParseRoot(target); err == nil { - if err := visit(target, depth+1); err != nil { - return err - } - heights[key] = max(heights[key], 1+heights[target.String()]) - } - } - next.objects[key] = o - delete(visiting, key) - return nil - } - if err := visit(root, 0); err != nil { - return nil, err - } - return next, nil -} - -// Preview runs closure and bounds checks before submission. It is local state, -// not evidence of persistence, publication, or trusted-root acceptance. -func (e *Edit) Preview(ctx context.Context, root cid.Cid) (View, error) { - g, err := e.graph(ctx, root) - if err != nil { - return View{}, err - } - return snapshot(g), nil -} -func (s *Session) Submit(ctx context.Context, transaction string, edit *Edit, root cid.Cid) (Result, error) { - s.mu.Lock() - defer s.mu.Unlock() - if edit == nil || edit.owner != s || edit.base != s.current { - return Result{}, errors.New("edit does not match current session state") - } - next, err := edit.graph(ctx, root) - if err != nil { - return Result{}, err - } - batch := protocol.AuthenticationBatch{Profile: protocol.AuthenticationBatchProfile, TransactionID: transaction, Base: s.current.root.String(), Root: root.String(), Candidates: edit.candidates} - if err := batch.Validate(); err != nil { - return Result{}, err - } - started := time.Now() - response, err := s.remote.SubmitAuthenticationBatch(ctx, batch) - roundTrip := elapsed(started) - if err != nil { - return Result{}, err - } - if err := response.Receipt.Validate(batch); err != nil { - return Result{}, err - } - if response.Receipt.DurableBoundary != gatewaytransport.AuthenticationDurableBoundary { - return Result{}, errors.New("receipt has an unexpected durable boundary") - } - result := Result{Base: s.current.root, Root: root, Receipt: response.Receipt, Idempotent: response.Idempotent, Computation: edit.metrics, Submission: response, SubmitRoundTripNS: roundTrip} - s.current = next - return result, nil -} -func cloneState(state engine.State) engine.State { - state.Entries = append([]engine.Entry{}, state.Entries...) - for i := range state.Entries { - state.Entries[i].Label = bytes.Clone(state.Entries[i].Label) - } - return state -} -func elapsed(started time.Time) uint64 { return uint64(max(time.Since(started), 0)) } - -func graphTargets(state engine.State) []cid.Cid { - targets := make([]cid.Cid, 0, len(state.Entries)+1) - if state.PayloadCID.Defined() { - targets = append(targets, state.PayloadCID) - } - for _, entry := range state.Entries { - targets = append(targets, entry.Target) - } - return targets -} diff --git a/internal/evaluation/authenticationgraph/graph_test.go b/internal/evaluation/authenticationgraph/graph_test.go deleted file mode 100644 index b747cde..0000000 --- a/internal/evaluation/authenticationgraph/graph_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package authenticationgraph - -import ( - "context" - "encoding/json" - "errors" - "testing" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-core/auth/commitment" - "github.com/dewebprotocol/malt-core/auth/commitment/kzg" - "github.com/dewebprotocol/malt-core/auth/coordinate" - "github.com/dewebprotocol/malt-core/derivation" - "github.com/dewebprotocol/malt-core/engine" - "github.com/dewebprotocol/malt-core/maltcid" - "github.com/dewebprotocol/malt-core/protocol" - "github.com/dewebprotocol/malt-core/sdk/authentication" - cid "github.com/ipfs/go-cid" -) - -type countedScheme struct { - *kzg.Scheme - verifies int -} - -func (s *countedScheme) BatchVerify(root commitment.Value, indices []uint64, values []commitment.Cell, proof []byte) (bool, error) { - s.verifies++ - return s.Scheme.BatchVerify(root, indices, values, proof) -} -func (s *countedScheme) VerifyIndex(root commitment.Value, index uint64, value commitment.Cell, proof []byte) (bool, error) { - s.verifies++ - return s.Scheme.VerifyIndex(root, index, value, proof) -} - -type fixtureRemote struct { - candidates map[string]protocol.AuthenticationCandidate - fetches, submits int - wrong string - last protocol.AuthenticationBatch -} - -func (r *fixtureRemote) AuthenticationCandidate(_ context.Context, root cid.Cid) (gatewaytransport.CandidateResponse, error) { - r.fetches++ - c, ok := r.candidates[root.String()] - if !ok { - return gatewaytransport.CandidateResponse{}, errors.New("missing candidate") - } - data, _ := json.Marshal(c) - c, _ = protocol.DecodeAuthenticationCandidate(data) - return gatewaytransport.CandidateResponse{Candidate: c, WireBytes: uint64(len(data))}, nil -} -func (r *fixtureRemote) SubmitAuthenticationBatch(_ context.Context, b protocol.AuthenticationBatch) (gatewaytransport.BatchResponse, error) { - r.submits++ - r.last = b - digest, err := b.Digest() - if err != nil { - return gatewaytransport.BatchResponse{}, err - } - receipt := protocol.AuthenticationReceipt{Profile: protocol.AuthenticationReceiptProfile, TransactionID: b.TransactionID, Base: b.Base, Root: b.Root, Digest: digest, DurableBoundary: gatewaytransport.AuthenticationDurableBoundary} - switch r.wrong { - case "root": - receipt.Root = b.Base - case "transaction": - receipt.TransactionID = "other" - case "digest": - receipt.Digest = "different" - case "boundary": - receipt.DurableBoundary = "different" - case "profile": - receipt.Profile = "old" - } - if r.wrong == "" { - for _, c := range b.Candidates { - r.candidates[c.Root] = c - } - } - return gatewaytransport.BatchResponse{Receipt: receipt}, nil -} -func graphFixture(t *testing.T) (*Session, *fixtureRemote, *countedScheme, cid.Cid, cid.Cid, cid.Cid) { - t.Helper() - ctx := context.Background() - base, err := kzg.NewScheme() - if err != nil { - t.Fatal(err) - } - counted := &countedScheme{Scheme: base} - registry := engine.NewRegistry() - if err := registry.Register(counted); err != nil { - t.Fatal(err) - } - e := engine.New(registry) - payload := raw(t, "first chunk") - child, err := authentication.Prepare(ctx, e, engine.State{Descriptor: maltcid.RootDescriptor{DerivationProfile: uint8(derivation.Direct), Layout: maltcid.Positional, Profile: maltcid.KZG4096}, Entries: []engine.Entry{{Label: coordinate.EncodeIndex(0), Target: payload}}}) - if err != nil { - t.Fatal(err) - } - childRoot, _ := cid.Parse(child.Root) - parent, err := authentication.Prepare(ctx, e, engine.State{Descriptor: maltcid.RootDescriptor{Layout: maltcid.Prefix, DerivationProfile: uint8(derivation.SHA256), Profile: maltcid.KZG4096}, Entries: []engine.Entry{{Label: []byte("first"), Target: childRoot}, {Label: []byte("alias"), Target: childRoot}}}) - if err != nil { - t.Fatal(err) - } - root, _ := cid.Parse(parent.Root) - remote := &fixtureRemote{candidates: map[string]protocol.AuthenticationCandidate{child.Root: child, parent.Root: parent}} - session, err := New(remote, e) - if err != nil { - t.Fatal(err) - } - return session, remote, counted, root, childRoot, payload -} -func raw(t *testing.T, s string) cid.Cid { - t.Helper() - v, err := clientcas.CIDForBlock(clientcas.Block{Codec: cid.Raw, Data: []byte(s)}) - if err != nil { - t.Fatal(err) - } - return v -} -func TestRetainedWritersCopyOnWriteAndExactReceipt(t *testing.T) { - s, r, scheme, root, child, payload := graphFixture(t) - ctx := context.Background() - load, err := s.Load(ctx, root) - if err != nil { - t.Fatal(err) - } - if load.Objects != 2 || r.fetches != 2 || scheme.verifies == 0 { - t.Fatalf("initial import not observed: %+v fetches=%d verifies=%d", load, r.fetches, scheme.verifies) - } - scheme.verifies = 0 - stale, _ := s.Begin() - edit, _ := s.Begin() - after := raw(t, "second data") - nextChild, err := edit.Apply(ctx, child, authentication.Delta{Changes: []engine.Change{{Label: coordinate.EncodeIndex(0), Before: payload, After: after}}}) - if err != nil { - t.Fatal(err) - } - next, err := edit.Apply(ctx, root, authentication.Delta{Changes: []engine.Change{{Label: []byte("first"), Before: child, After: nextChild}}}) - if err != nil { - t.Fatal(err) - } - preview, err := edit.Preview(ctx, next) - if err != nil { - t.Fatal(err) - } - if len(preview.States) != 3 { - t.Fatal("aliased child was not preserved") - } - state, _ := preview.State(next) - for _, entry := range state.Entries { - if string(entry.Label) == "alias" && !entry.Target.Equals(child) { - t.Fatal("alias was rebound") - } - } - // A snapshot is caller-owned; changing its input or target must not poison a writer. - old, _ := s.Snapshot() - oldState := old.States[root.String()] - oldState.Entries[0].Label[0] ^= 1 - oldState.Entries[0].Target = after - for _, bad := range []string{"root", "transaction", "digest", "boundary", "profile"} { - r.wrong = bad - if _, err := s.Submit(ctx, "edit-one", edit, next); err == nil { - t.Fatalf("accepted %s substitution", bad) - } - unchanged, _ := s.Snapshot() - if !unchanged.Root.Equals(root) { - t.Fatal("failed receipt advanced session") - } - } - r.wrong = "" - result, err := s.Submit(ctx, "edit-one", edit, next) - if err != nil { - t.Fatal(err) - } - if !result.Root.Equals(next) || r.last.Candidates[0].Root != nextChild.String() || r.last.Candidates[1].Root != next.String() { - t.Fatal("dependency order or receipt Root differs") - } - if r.fetches != 2 || scheme.verifies != 0 { - t.Fatalf("incremental writer refetched or reverified state: fetches=%d verifies=%d", r.fetches, scheme.verifies) - } - if _, err := s.Submit(ctx, "stale-edit", stale, root); err == nil { - t.Fatal("stale edit accepted") - } - if err := s.Audit(ctx); err != nil { - t.Fatal(err) - } - if r.fetches != 5 || scheme.verifies == 0 { - t.Fatal("durable audit did not independently import full closure") - } -} -func TestFailedImportAndMissingClosureLeavePriorSessionUsable(t *testing.T) { - s, r, _, root, child, _ := graphFixture(t) - ctx := context.Background() - if _, err := s.Load(ctx, root); err != nil { - t.Fatal(err) - } - c := r.candidates[child.String()] - delete(r.candidates, child.String()) - if _, err := s.Load(ctx, root); err == nil { - t.Fatal("missing child accepted") - } - state, err := s.Snapshot() - if err != nil || !state.Root.Equals(root) { - t.Fatal("failed load discarded prior state") - } - r.candidates[child.String()] = c - bad := r.candidates[root.String()] - bad.State.Entries = append([]engine.Entry{}, bad.State.Entries...) - bad.State.Entries[0].Target = raw(t, "wrong") - r.candidates[root.String()] = bad - if _, err := s.Load(ctx, root); err == nil { - t.Fatal("unbound inputs accepted") - } - edit, _ := s.Begin() - unknown := raw(t, "not a Root") - if _, err := edit.Apply(ctx, unknown, authentication.Delta{}); err == nil { - t.Fatal("unknown edit base accepted") - } -} diff --git a/internal/evaluation/e0selftest/e0selftest.go b/internal/evaluation/e0selftest/e0selftest.go deleted file mode 100644 index 829e54e..0000000 --- a/internal/evaluation/e0selftest/e0selftest.go +++ /dev/null @@ -1,614 +0,0 @@ -// Package e0selftest implements the cross-repository side of the evaluator's -// formal-E0 invocation/receipt contract. Evaluation commands use it only when -// the reserved invocation environment is present; normal product paths never -// depend on this package. -package e0selftest - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "regexp" - "slices" - "strings" -) - -const ( - InvocationEnvironment = "MALT_EVAL_E0_SELF_TEST_INVOCATION" - InvocationSchema = "malt-eval-e0-self-test-invocation/v1" - ReceiptSchema = "malt-eval-e0-self-test-receipt/v1" - maximumInvocationSize = 1 << 20 -) - -var ( - idPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,127}$`) - hex64 = regexp.MustCompile(`^[0-9a-f]{64}$`) -) - -type Profile struct { - ProfileID string `json:"profile_id"` - PositiveCases []string `json:"positive_cases"` - HostileCases []string `json:"hostile_cases"` -} - -type Contract struct { - ProfileID string `json:"profile_id"` - ProfileSHA256 string `json:"profile_sha256"` - PositiveCases uint32 `json:"positive_cases"` - HostileCases uint32 `json:"hostile_cases"` -} - -type CaseResult struct { - ID string - Passed bool -} - -type Counts struct { - Expected uint32 `json:"expected"` - Executed uint32 `json:"executed"` - Passed uint32 `json:"passed"` -} - -type TestedFilePin struct { - FileID string `json:"file_id"` - SHA256 string `json:"sha256"` - Bytes int64 `json:"bytes"` -} - -type Receipt struct { - SchemaVersion string `json:"schema_version"` - CapabilityID string `json:"capability_id"` - ProfileID string `json:"profile_id"` - ProfileSHA256 string `json:"profile_sha256"` - CorpusSHA256 string `json:"corpus_sha256"` - Positive Counts `json:"positive"` - Hostile Counts `json:"hostile"` - TestedExecutable TestedFilePin `json:"tested_executable"` - TestedInputs []TestedFilePin `json:"tested_inputs"` -} - -type invocation struct { - SchemaVersion string `json:"schema_version"` - CapabilityID string `json:"capability_id"` - Contract Contract `json:"contract"` - CorpusSHA256 string `json:"corpus_sha256"` - Executable invocationFile `json:"executable"` - Inputs []invocationFile `json:"inputs"` -} - -type invocationFile struct { - FileID string `json:"file_id"` - Path string `json:"path"` - SHA256 string `json:"sha256"` - Bytes int64 `json:"bytes"` -} - -type InputFile struct { - FileID string - Path string -} - -// InputPaths returns the exact canonical input paths in the evaluator-authored -// invocation. Self-test commands use it to prove that their typed file flags -// consume the complete argument set, rather than relying on Issue to bind -// inputs they never opened or interpreted. -func InputPaths() ([]string, error) { - invocation, err := readInvocation() - if err != nil { - return nil, err - } - paths := make([]string, len(invocation.Inputs)) - for index, input := range invocation.Inputs { - paths[index] = input.Path - } - slices.Sort(paths) - return paths, nil -} - -// Issue verifies the evaluator-authored invocation, the compiled capability -// and profile, every executed case, the running binary, and every file input. -// It returns no receipt if any positive or hostile case is missing or fails. -func Issue(expectedCapabilityID string, profile Profile, results []CaseResult) (Receipt, error) { - if !idPattern.MatchString(expectedCapabilityID) { - return Receipt{}, fmt.Errorf("invalid compiled self-test capability ID %q", expectedCapabilityID) - } - profileContract, err := profile.Contract() - if err != nil { - return Receipt{}, err - } - invocation, err := readInvocation() - if err != nil { - return Receipt{}, err - } - if invocation.CapabilityID != expectedCapabilityID { - return Receipt{}, fmt.Errorf("compiled self-test capability %q does not match E0 invocation %q", expectedCapabilityID, invocation.CapabilityID) - } - if invocation.Contract != profileContract { - return Receipt{}, fmt.Errorf("compiled self-test profile %+v does not match E0 contract %+v", profileContract, invocation.Contract) - } - positive, hostile, err := countCases(profile, results) - if err != nil { - return Receipt{}, err - } - executable, err := describeExecutable(invocation.Executable) - if err != nil { - return Receipt{}, err - } - inputs := make([]TestedFilePin, len(invocation.Inputs)) - for index, input := range invocation.Inputs { - pin, err := describeInvocationFile(input) - if err != nil { - return Receipt{}, fmt.Errorf("self-test input %q: %w", input.FileID, err) - } - inputs[index] = pin - } - slices.SortFunc(inputs, func(left, right TestedFilePin) int { return strings.Compare(left.FileID, right.FileID) }) - receipt := Receipt{ - SchemaVersion: ReceiptSchema, CapabilityID: expectedCapabilityID, - ProfileID: profileContract.ProfileID, ProfileSHA256: profileContract.ProfileSHA256, - CorpusSHA256: invocation.CorpusSHA256, Positive: positive, Hostile: hostile, - TestedExecutable: executable, TestedInputs: inputs, - } - if err := receipt.Validate(); err != nil { - return Receipt{}, err - } - return receipt, nil -} - -func (p Profile) Contract() (Contract, error) { - if !idPattern.MatchString(p.ProfileID) { - return Contract{}, fmt.Errorf("invalid self-test profile ID %q", p.ProfileID) - } - if err := validateCaseIDs("positive", p.PositiveCases); err != nil { - return Contract{}, err - } - if err := validateCaseIDs("hostile", p.HostileCases); err != nil { - return Contract{}, err - } - seen := make(map[string]struct{}, len(p.PositiveCases)+len(p.HostileCases)) - for _, id := range append(append([]string(nil), p.PositiveCases...), p.HostileCases...) { - if _, duplicate := seen[id]; duplicate { - return Contract{}, fmt.Errorf("self-test case %q appears in both profiles or repeats", id) - } - seen[id] = struct{}{} - } - digest, err := canonicalDigest(p) - if err != nil { - return Contract{}, err - } - return Contract{ProfileID: p.ProfileID, ProfileSHA256: digest, PositiveCases: uint32(len(p.PositiveCases)), HostileCases: uint32(len(p.HostileCases))}, nil -} - -func (r Receipt) Validate() error { - if r.SchemaVersion != ReceiptSchema || !idPattern.MatchString(r.CapabilityID) || !idPattern.MatchString(r.ProfileID) || !hex64.MatchString(r.ProfileSHA256) || !hex64.MatchString(r.CorpusSHA256) { - return errors.New("invalid E0 self-test receipt identity") - } - if err := validateCounts("positive", r.Positive); err != nil { - return err - } - if err := validateCounts("hostile", r.Hostile); err != nil { - return err - } - if err := r.TestedExecutable.validate(); err != nil { - return fmt.Errorf("invalid tested executable: %w", err) - } - if len(r.TestedInputs) == 0 { - return errors.New("self-test receipt must bind at least one input") - } - for index, input := range r.TestedInputs { - if err := input.validate(); err != nil { - return fmt.Errorf("invalid tested input: %w", err) - } - if input.FileID == r.TestedExecutable.FileID { - return errors.New("self-test receipt repeats executable as an input") - } - if index > 0 && r.TestedInputs[index-1].FileID >= input.FileID { - return errors.New("self-test receipt inputs must be canonical sorted unique file IDs") - } - } - return nil -} - -// BuildInvocationEnvironment is a test/interop helper. It derives all digests -// from live files and returns the exact reserved environment value used by the -// evaluator; callers cannot provide claimed file hashes. -func BuildInvocationEnvironment(capabilityID string, profile Profile, executable InputFile, inputs []InputFile, corpusFileIDs []string) (string, error) { - contract, err := profile.Contract() - if err != nil { - return "", err - } - executablePin, err := describeInputFile(executable) - if err != nil { - return "", err - } - inputPins := make([]invocationFile, len(inputs)) - byID := make(map[string]invocationFile, len(inputs)) - for index, input := range inputs { - pin, err := describeInputFile(input) - if err != nil { - return "", err - } - if _, duplicate := byID[pin.FileID]; duplicate { - return "", fmt.Errorf("repeated self-test input file %q", pin.FileID) - } - byID[pin.FileID] = pin - inputPins[index] = pin - } - slices.SortFunc(inputPins, func(left, right invocationFile) int { return strings.Compare(left.FileID, right.FileID) }) - corpusPins := make([]TestedFilePin, len(corpusFileIDs)) - seenCorpus := make(map[string]struct{}, len(corpusFileIDs)) - for index, id := range corpusFileIDs { - pin, exists := byID[id] - if !exists { - return "", fmt.Errorf("corpus file %q is not a self-test input", id) - } - if _, duplicate := seenCorpus[id]; duplicate { - return "", fmt.Errorf("repeated self-test corpus file %q", id) - } - seenCorpus[id] = struct{}{} - corpusPins[index] = TestedFilePin{FileID: pin.FileID, SHA256: pin.SHA256, Bytes: pin.Bytes} - } - if len(corpusPins) == 0 { - return "", errors.New("self-test corpus set is empty") - } - slices.SortFunc(corpusPins, func(left, right TestedFilePin) int { return strings.Compare(left.FileID, right.FileID) }) - corpusDigest, err := canonicalDigest(corpusPins) - if err != nil { - return "", err - } - invocation := invocation{ - SchemaVersion: InvocationSchema, CapabilityID: capabilityID, Contract: contract, CorpusSHA256: corpusDigest, - Executable: executablePin, Inputs: inputPins, - } - if err := invocation.validate(); err != nil { - return "", err - } - encoded, err := marshalCanonical(invocation) - if err != nil { - return "", err - } - return string(encoded), nil -} - -func readInvocation() (invocation, error) { - raw := os.Getenv(InvocationEnvironment) - if raw == "" { - return invocation{}, fmt.Errorf("%s is required for a formal E0 self-test", InvocationEnvironment) - } - if len(raw) > maximumInvocationSize { - return invocation{}, errors.New("E0 self-test invocation exceeds the bounded profile") - } - var value invocation - if err := decodeStrict([]byte(raw), &value); err != nil { - return invocation{}, fmt.Errorf("decode E0 self-test invocation: %w", err) - } - if err := value.validate(); err != nil { - return invocation{}, err - } - return value, nil -} - -func (i invocation) validate() error { - if i.SchemaVersion != InvocationSchema || !idPattern.MatchString(i.CapabilityID) || !hex64.MatchString(i.CorpusSHA256) { - return errors.New("invalid E0 self-test invocation identity") - } - if err := i.Contract.validate(); err != nil { - return err - } - if err := i.Executable.validate(); err != nil { - return fmt.Errorf("invalid E0 self-test executable: %w", err) - } - if len(i.Inputs) == 0 { - return errors.New("E0 external self-test invocation requires input pins") - } - for index, input := range i.Inputs { - if err := input.validate(); err != nil { - return fmt.Errorf("invalid E0 self-test input: %w", err) - } - if input.FileID == i.Executable.FileID { - return errors.New("E0 self-test invocation repeats executable as an input") - } - if index > 0 && i.Inputs[index-1].FileID >= input.FileID { - return errors.New("E0 self-test inputs must be canonical sorted unique file IDs") - } - } - return nil -} - -func (c Contract) validate() error { - if !idPattern.MatchString(c.ProfileID) || !hex64.MatchString(c.ProfileSHA256) || c.PositiveCases == 0 || c.HostileCases == 0 { - return errors.New("self-test contract requires a canonical profile, digest, and nonzero positive/hostile counts") - } - return nil -} - -func (f invocationFile) validate() error { - if !idPattern.MatchString(f.FileID) || !filepath.IsAbs(f.Path) || filepath.Clean(f.Path) != f.Path || !hex64.MatchString(f.SHA256) || f.Bytes < 0 { - return fmt.Errorf("invalid invocation file %q", f.FileID) - } - return nil -} - -func (f TestedFilePin) validate() error { - if !idPattern.MatchString(f.FileID) || !hex64.MatchString(f.SHA256) || f.Bytes < 0 { - return fmt.Errorf("invalid tested file %q", f.FileID) - } - return nil -} - -func validateCounts(label string, counts Counts) error { - if counts.Expected == 0 || counts.Executed != counts.Expected || counts.Passed != counts.Expected { - return fmt.Errorf("self-test %s counts must be nonzero and satisfy expected=executed=passed", label) - } - return nil -} - -func validateCaseIDs(label string, values []string) error { - if len(values) == 0 { - return fmt.Errorf("self-test %s profile must contain at least one case", label) - } - seen := make(map[string]struct{}, len(values)) - for _, id := range values { - if !idPattern.MatchString(id) { - return fmt.Errorf("invalid self-test %s case ID %q", label, id) - } - if _, duplicate := seen[id]; duplicate { - return fmt.Errorf("repeated self-test %s case ID %q", label, id) - } - seen[id] = struct{}{} - } - return nil -} - -func countCases(profile Profile, results []CaseResult) (Counts, Counts, error) { - positiveIDs := make(map[string]struct{}, len(profile.PositiveCases)) - hostileIDs := make(map[string]struct{}, len(profile.HostileCases)) - for _, id := range profile.PositiveCases { - positiveIDs[id] = struct{}{} - } - for _, id := range profile.HostileCases { - hostileIDs[id] = struct{}{} - } - positive := Counts{Expected: uint32(len(profile.PositiveCases))} - hostile := Counts{Expected: uint32(len(profile.HostileCases))} - seen := make(map[string]struct{}, len(results)) - for _, result := range results { - if _, duplicate := seen[result.ID]; duplicate { - return Counts{}, Counts{}, fmt.Errorf("self-test case %q executed more than once", result.ID) - } - seen[result.ID] = struct{}{} - switch { - case contains(positiveIDs, result.ID): - positive.Executed++ - if result.Passed { - positive.Passed++ - } - case contains(hostileIDs, result.ID): - hostile.Executed++ - if result.Passed { - hostile.Passed++ - } - default: - return Counts{}, Counts{}, fmt.Errorf("self-test executed unknown case %q", result.ID) - } - } - if positive.Executed != positive.Expected || hostile.Executed != hostile.Expected { - return Counts{}, Counts{}, fmt.Errorf("self-test executed positive=%d/%d hostile=%d/%d", positive.Executed, positive.Expected, hostile.Executed, hostile.Expected) - } - if positive.Passed != positive.Expected || hostile.Passed != hostile.Expected { - return Counts{}, Counts{}, fmt.Errorf("self-test passed positive=%d/%d hostile=%d/%d", positive.Passed, positive.Expected, hostile.Passed, hostile.Expected) - } - return positive, hostile, nil -} - -func contains(values map[string]struct{}, id string) bool { - _, ok := values[id] - return ok -} - -func describeExecutable(expected invocationFile) (TestedFilePin, error) { - path, err := os.Executable() - if err != nil { - return TestedFilePin{}, fmt.Errorf("locate self-test executable: %w", err) - } - actualInfo, err := os.Stat(path) - if err != nil { - return TestedFilePin{}, fmt.Errorf("inspect running self-test executable: %w", err) - } - expectedInfo, err := os.Stat(expected.Path) - if err != nil { - return TestedFilePin{}, fmt.Errorf("inspect E0 executable pin: %w", err) - } - if !os.SameFile(actualInfo, expectedInfo) { - return TestedFilePin{}, errors.New("running self-test executable is not the E0-pinned file") - } - return describeInvocationFile(expected) -} - -func describeInputFile(file InputFile) (invocationFile, error) { - if !idPattern.MatchString(file.FileID) || !filepath.IsAbs(file.Path) || filepath.Clean(file.Path) != file.Path { - return invocationFile{}, fmt.Errorf("invalid self-test input file %q", file.FileID) - } - digest, size, err := describeRegularFile(file.Path) - if err != nil { - return invocationFile{}, err - } - return invocationFile{FileID: file.FileID, Path: file.Path, SHA256: digest, Bytes: size}, nil -} - -func describeInvocationFile(expected invocationFile) (TestedFilePin, error) { - digest, size, err := describeRegularFile(expected.Path) - if err != nil { - return TestedFilePin{}, err - } - if digest != expected.SHA256 || size != expected.Bytes { - return TestedFilePin{}, fmt.Errorf("live descriptor %s/%d does not match E0 pin %s/%d", digest, size, expected.SHA256, expected.Bytes) - } - return TestedFilePin{FileID: expected.FileID, SHA256: digest, Bytes: size}, nil -} - -func describeRegularFile(path string) (string, int64, error) { - if !filepath.IsAbs(path) || filepath.Clean(path) != path { - return "", 0, errors.New("self-test file path must be canonical and absolute") - } - info, err := os.Lstat(path) - if err != nil { - return "", 0, err - } - if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() { - return "", 0, errors.New("self-test file must be a regular non-symlink") - } - resolved, err := filepath.EvalSymlinks(path) - if err != nil { - return "", 0, err - } - if resolved != path { - return "", 0, errors.New("self-test file or one of its parents is symlinked") - } - file, err := os.Open(path) - if err != nil { - return "", 0, err - } - defer file.Close() - opened, err := file.Stat() - if err != nil { - return "", 0, err - } - if !os.SameFile(info, opened) || !opened.Mode().IsRegular() { - return "", 0, errors.New("self-test file changed while opening") - } - hash := sha256.New() - written, err := io.Copy(hash, file) - if err != nil { - return "", 0, err - } - if written != opened.Size() { - return "", 0, errors.New("self-test file size changed while hashing") - } - return hex.EncodeToString(hash.Sum(nil)), written, nil -} - -func canonicalDigest(value any) (string, error) { - encoded, err := marshalCanonical(value) - if err != nil { - return "", err - } - digest := sha256.Sum256(encoded) - return hex.EncodeToString(digest[:]), nil -} - -func marshalCanonical(value any) ([]byte, error) { - var initial bytes.Buffer - encoder := json.NewEncoder(&initial) - encoder.SetEscapeHTML(false) - if err := encoder.Encode(value); err != nil { - return nil, err - } - decoder := json.NewDecoder(bytes.NewReader(bytes.TrimSuffix(initial.Bytes(), []byte{'\n'}))) - decoder.UseNumber() - var normalized any - if err := decoder.Decode(&normalized); err != nil { - return nil, err - } - if err := requireEOF(decoder); err != nil { - return nil, err - } - var output bytes.Buffer - encoder = json.NewEncoder(&output) - encoder.SetEscapeHTML(false) - if err := encoder.Encode(normalized); err != nil { - return nil, err - } - return bytes.TrimSuffix(output.Bytes(), []byte{'\n'}), nil -} - -func decodeStrict(data []byte, target any) error { - trimmed := bytes.TrimSpace(data) - if len(trimmed) == 0 || trimmed[0] != '{' { - return errors.New("top-level JSON value must be an object") - } - if err := rejectDuplicateKeys(data); err != nil { - return err - } - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.DisallowUnknownFields() - if err := decoder.Decode(target); err != nil { - return err - } - return requireEOF(decoder) -} - -func rejectDuplicateKeys(data []byte) error { - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.UseNumber() - if err := scanValue(decoder, "$"); err != nil { - return err - } - return requireEOF(decoder) -} - -func scanValue(decoder *json.Decoder, path string) error { - token, err := decoder.Token() - if err != nil { - return err - } - delimiter, ok := token.(json.Delim) - if !ok { - return nil - } - switch delimiter { - case '{': - seen := make(map[string]struct{}) - for decoder.More() { - keyToken, err := decoder.Token() - if err != nil { - return err - } - key, ok := keyToken.(string) - if !ok { - return fmt.Errorf("object key at %s is not a string", path) - } - if _, duplicate := seen[key]; duplicate { - return fmt.Errorf("duplicate JSON object key %q at %s", key, path) - } - seen[key] = struct{}{} - if err := scanValue(decoder, path+"."+key); err != nil { - return err - } - } - closing, err := decoder.Token() - if err != nil || closing != json.Delim('}') { - return errors.New("invalid JSON object closing delimiter") - } - case '[': - for index := 0; decoder.More(); index++ { - if err := scanValue(decoder, fmt.Sprintf("%s[%d]", path, index)); err != nil { - return err - } - } - closing, err := decoder.Token() - if err != nil || closing != json.Delim(']') { - return errors.New("invalid JSON array closing delimiter") - } - default: - return fmt.Errorf("unexpected JSON delimiter %q", delimiter) - } - return nil -} - -func requireEOF(decoder *json.Decoder) error { - var trailing json.RawMessage - if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { - if err == nil { - return errors.New("trailing JSON value") - } - return err - } - return nil -} diff --git a/internal/evaluation/e0selftest/e0selftest_test.go b/internal/evaluation/e0selftest/e0selftest_test.go deleted file mode 100644 index 697c1c1..0000000 --- a/internal/evaluation/e0selftest/e0selftest_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package e0selftest - -import ( - "os" - "path/filepath" - "strings" - "testing" -) - -func TestIssueBindsEvaluatorInvocationCasesAndFiles(t *testing.T) { - profile := Profile{ProfileID: "profile", PositiveCases: []string{"accept"}, HostileCases: []string{"reject"}} - contract, err := profile.Contract() - if err != nil { - t.Fatal(err) - } - if contract.ProfileSHA256 != "254f4b991b9ae62cc380181d4d0daba8d7019b312c620c42340e2bbea1451ad8" { - t.Fatalf("profile digest = %s", contract.ProfileSHA256) - } - input := filepath.Join(t.TempDir(), "corpus.json") - if err := os.WriteFile(input, []byte(`{"case":"fixed"}`), 0o600); err != nil { - t.Fatal(err) - } - executable, err := os.Executable() - if err != nil { - t.Fatal(err) - } - environment, err := BuildInvocationEnvironment( - "capability", profile, - InputFile{FileID: "worker", Path: executable}, - []InputFile{{FileID: "corpus", Path: input}}, - []string{"corpus"}, - ) - if err != nil { - t.Fatal(err) - } - t.Setenv(InvocationEnvironment, environment) - receipt, err := Issue("capability", profile, []CaseResult{{ID: "accept", Passed: true}, {ID: "reject", Passed: true}}) - if err != nil { - t.Fatal(err) - } - if err := receipt.Validate(); err != nil { - t.Fatal(err) - } - if receipt.CapabilityID != "capability" || receipt.Positive.Executed != 1 || receipt.Hostile.Executed != 1 || len(receipt.TestedInputs) != 1 || receipt.TestedInputs[0].FileID != "corpus" { - t.Fatalf("receipt = %+v", receipt) - } -} - -func TestIssueRejectsRelabelMissingFailedAndDuplicateCases(t *testing.T) { - profile := Profile{ProfileID: "profile", PositiveCases: []string{"accept"}, HostileCases: []string{"reject"}} - input := filepath.Join(t.TempDir(), "corpus.json") - if err := os.WriteFile(input, []byte(`{"case":"fixed"}`), 0o600); err != nil { - t.Fatal(err) - } - executable, err := os.Executable() - if err != nil { - t.Fatal(err) - } - environment, err := BuildInvocationEnvironment( - "capability", profile, - InputFile{FileID: "worker", Path: executable}, - []InputFile{{FileID: "corpus", Path: input}}, - []string{"corpus"}, - ) - if err != nil { - t.Fatal(err) - } - t.Setenv(InvocationEnvironment, environment) - for name, test := range map[string]struct { - capability string - results []CaseResult - }{ - "relabel": {capability: "other", results: []CaseResult{{ID: "accept", Passed: true}, {ID: "reject", Passed: true}}}, - "missing": {capability: "capability", results: []CaseResult{{ID: "accept", Passed: true}}}, - "failed": {capability: "capability", results: []CaseResult{{ID: "accept", Passed: true}, {ID: "reject", Passed: false}}}, - "duplicate": {capability: "capability", results: []CaseResult{{ID: "accept", Passed: true}, {ID: "accept", Passed: true}, {ID: "reject", Passed: true}}}, - } { - t.Run(name, func(t *testing.T) { - if _, err := Issue(test.capability, profile, test.results); err == nil { - t.Fatal("invalid self-test issued a receipt") - } - }) - } -} - -func TestIssueRejectsMutatedInputAndMalformedInvocation(t *testing.T) { - profile := Profile{ProfileID: "profile", PositiveCases: []string{"accept"}, HostileCases: []string{"reject"}} - input := filepath.Join(t.TempDir(), "corpus.json") - if err := os.WriteFile(input, []byte(`{"case":"fixed"}`), 0o600); err != nil { - t.Fatal(err) - } - executable, err := os.Executable() - if err != nil { - t.Fatal(err) - } - environment, err := BuildInvocationEnvironment( - "capability", profile, - InputFile{FileID: "worker", Path: executable}, - []InputFile{{FileID: "corpus", Path: input}}, - []string{"corpus"}, - ) - if err != nil { - t.Fatal(err) - } - if err := os.WriteFile(input, []byte(`{"case":"mutated"}`), 0o600); err != nil { - t.Fatal(err) - } - t.Setenv(InvocationEnvironment, environment) - if _, err := Issue("capability", profile, []CaseResult{{ID: "accept", Passed: true}, {ID: "reject", Passed: true}}); err == nil || !strings.Contains(err.Error(), "does not match E0 pin") { - t.Fatalf("mutated input error = %v", err) - } - t.Setenv(InvocationEnvironment, `{"schema_version":"one","schema_version":"two"}`) - if _, err := Issue("capability", profile, []CaseResult{{ID: "accept", Passed: true}, {ID: "reject", Passed: true}}); err == nil || !strings.Contains(err.Error(), "duplicate") { - t.Fatalf("duplicate invocation error = %v", err) - } -} diff --git a/internal/evaluation/gatewaytransport/accounting.go b/internal/evaluation/gatewaytransport/accounting.go deleted file mode 100644 index 0a8cf35..0000000 --- a/internal/evaluation/gatewaytransport/accounting.go +++ /dev/null @@ -1,52 +0,0 @@ -package gatewaytransport - -import "math" - -// WriteAccounting is untrusted Gateway diagnostic accounting for -// the atomic staging boundary. It excludes payload CAS and physical backend -// bytes, which evaluation workers must account for separately. -type WriteAccounting struct { - Profile string `json:"profile"` - Available bool `json:"available"` - UnavailableReason string `json:"unavailable_reason,omitempty"` - ByteMethod string `json:"byte_method"` - ObjectLedgerSHA256 string `json:"object_ledger_sha256,omitempty"` - Categories []WriteCategoryAccounting `json:"categories"` -} - -type WriteCategoryAccounting struct { - Category string `json:"category"` - AttemptedWrites uint64 `json:"attempted_writes"` - AttemptedBytes uint64 `json:"attempted_bytes"` - AttemptedNewWrites uint64 `json:"attempted_new_writes"` - AttemptedNewBytes uint64 `json:"attempted_new_bytes"` - AttemptedReplacementWrites uint64 `json:"attempted_replacement_writes"` - AttemptedReplacementBytes uint64 `json:"attempted_replacement_bytes"` - AttemptedSameValueWrites uint64 `json:"attempted_same_value_writes"` - AttemptedSameValueBytes uint64 `json:"attempted_same_value_bytes"` - AttemptedDeleteWrites uint64 `json:"attempted_delete_writes"` - AttemptedDeleteBytes uint64 `json:"attempted_delete_bytes"` - NewlyPersistedWrites uint64 `json:"newly_persisted_writes"` - GrossNewBytes uint64 `json:"gross_new_bytes"` - NewWrites uint64 `json:"new_writes"` - NewBytes uint64 `json:"new_bytes"` - ReplacedWrites uint64 `json:"replaced_writes"` - ReplacementNewBytes uint64 `json:"replacement_new_bytes"` - ReplacementReclaimedBytes uint64 `json:"replacement_reclaimed_bytes"` - SameValueWrites uint64 `json:"same_value_writes"` - DeletedWrites uint64 `json:"deleted_writes"` - DeletedReclaimedBytes uint64 `json:"deleted_reclaimed_bytes"` - ReclaimedBytes uint64 `json:"reclaimed_bytes"` - NetBytes int64 `json:"net_bytes"` -} - -func sumAmounts(want uint64, values ...uint64) bool { - var total uint64 - for _, value := range values { - if value > math.MaxUint64-total { - return false - } - total += value - } - return want == total -} diff --git a/internal/evaluation/gatewaytransport/authentication.go b/internal/evaluation/gatewaytransport/authentication.go deleted file mode 100644 index e6bff90..0000000 --- a/internal/evaluation/gatewaytransport/authentication.go +++ /dev/null @@ -1,206 +0,0 @@ -package gatewaytransport - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "net/http" - "net/url" - "strconv" - "strings" - "time" - - "github.com/dewebprotocol/malt-core/maltcid" - "github.com/dewebprotocol/malt-core/protocol" - cid "github.com/ipfs/go-cid" -) - -const ( - AuthenticationPhaseProfile = "gateway.authentication-phases/0" - AuthenticationDurableBoundary = "gateway.evaluation-authentication-kv-atomic/0" - authenticationHeaderPrefix = "X-Malt-Authentication-" -) - -// These observations describe one evaluation request. They are neither -// authentication evidence nor a root-publication or local trust decision. -type PhaseMetrics struct { - ValidationAndStageNS uint64 - PersistNS uint64 - ReceiptNS uint64 -} -type CandidateResponse struct { - Candidate protocol.AuthenticationCandidate - WireBytes uint64 -} -type BatchResponse struct { - Receipt protocol.AuthenticationReceipt - RequestWireBytes uint64 - ResponseWireBytes uint64 - RequestEncodingNS uint64 - ResponseVerifyNS uint64 - Idempotent bool - Gateway PhaseMetrics - WriteAccounting WriteAccounting - WriteAccountingWireBytes uint64 -} - -func (c *Client) AuthenticationCandidate(ctx context.Context, root cid.Cid) (CandidateResponse, error) { - if _, _, err := maltcid.ParseRoot(root); err != nil { - return CandidateResponse{}, err - } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.endpoint("/v1/evaluation/authentication/candidates/"+url.PathEscape(root.String())), nil) - if err != nil { - return CandidateResponse{}, err - } - response, err := c.instanceHTTP.Do(req) - if err != nil { - return CandidateResponse{}, err - } - defer response.Body.Close() - if response.StatusCode != http.StatusOK { - return CandidateResponse{}, c.responseError(response) - } - if err := requireJSONNoStore(response); err != nil { - return CandidateResponse{}, err - } - raw, err := readBounded(response.Body, min(c.maxJSONResponseBytes, int64(protocol.MaxVerificationJSONBytes)), "evaluation authentication candidate") - if err != nil { - return CandidateResponse{}, err - } - candidate, err := protocol.DecodeAuthenticationCandidate(raw) - if err != nil { - return CandidateResponse{}, err - } - if candidate.Root != root.String() { - return CandidateResponse{}, fmt.Errorf("candidate differs from caller-selected Root") - } - return CandidateResponse{Candidate: candidate, WireBytes: uint64(len(raw))}, nil -} - -// SubmitAuthenticationBatch validates the operational receipt against the -// exact submitted bytes/Roots/transaction. Local acceptance remains separate. -func (c *Client) SubmitAuthenticationBatch(ctx context.Context, batch protocol.AuthenticationBatch) (BatchResponse, error) { - started := time.Now() - if err := batch.Validate(); err != nil { - return BatchResponse{}, err - } - raw, err := json.Marshal(batch) - if err != nil { - return BatchResponse{}, err - } - if len(raw) > protocol.MaxVerificationJSONBytes { - return BatchResponse{}, fmt.Errorf("authentication batch exceeds protocol limit") - } - encodingNS := uint64(time.Since(started).Nanoseconds()) - request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint("/v1/evaluation/authentication/batches"), bytes.NewReader(raw)) - if err != nil { - return BatchResponse{}, err - } - request.Header.Set("Content-Type", "application/json") - response, err := c.instanceHTTP.Do(request) - if err != nil { - return BatchResponse{}, err - } - defer response.Body.Close() - if response.StatusCode != http.StatusCreated { - return BatchResponse{}, c.responseError(response) - } - if err := requireJSONNoStore(response); err != nil { - return BatchResponse{}, err - } - for name, want := range map[string]string{"Phase-Profile": AuthenticationPhaseProfile, "Publication": "separate", "Trust": "client-owned", "Durable-Boundary": AuthenticationDurableBoundary} { - if value, err := oneHeader(response.Header, authenticationHeaderPrefix+name); err != nil || value != want { - return BatchResponse{}, fmt.Errorf("unexpected authentication %s boundary", name) - } - } - phases, err := parseAuthenticationPhases(response.Header) - if err != nil { - return BatchResponse{}, err - } - accounting, accountingBytes, err := parseAuthenticationAccounting(response.Header) - if err != nil { - return BatchResponse{}, err - } - idempotent, err := oneHeader(response.Header, authenticationHeaderPrefix+"Idempotent") - if err != nil || idempotent != "true" && idempotent != "false" { - return BatchResponse{}, fmt.Errorf("missing canonical authentication idempotency classification") - } - encodedReceipt, err := readBounded(response.Body, min(c.maxJSONResponseBytes, int64(protocol.MaxVerificationJSONBytes)), "evaluation authentication receipt") - if err != nil { - return BatchResponse{}, err - } - verifyStarted := time.Now() - receipt, err := protocol.DecodeAuthenticationReceipt(encodedReceipt) - if err != nil { - return BatchResponse{}, err - } - if err := receipt.Validate(batch); err != nil { - return BatchResponse{}, err - } - if receipt.DurableBoundary != AuthenticationDurableBoundary { - return BatchResponse{}, fmt.Errorf("receipt differs from evaluation durable boundary") - } - return BatchResponse{Receipt: receipt, RequestWireBytes: uint64(len(raw)), ResponseWireBytes: uint64(len(encodedReceipt)), RequestEncodingNS: encodingNS, ResponseVerifyNS: uint64(time.Since(verifyStarted).Nanoseconds()), Idempotent: idempotent == "true", Gateway: phases, WriteAccounting: accounting, WriteAccountingWireBytes: accountingBytes}, nil -} - -func oneHeader(header http.Header, name string) (string, error) { - values := header.Values(name) - if len(values) != 1 || values[0] == "" || strings.TrimSpace(values[0]) != values[0] { - return "", fmt.Errorf("missing single canonical %s header", name) - } - return values[0], nil -} -func parseAuthenticationPhases(header http.Header) (PhaseMetrics, error) { - var result PhaseMetrics - for name, out := range map[string]*uint64{"Validation-And-Stage-Nanos": &result.ValidationAndStageNS, "Persist-Nanos": &result.PersistNS, "Receipt-Nanos": &result.ReceiptNS} { - raw, err := oneHeader(header, authenticationHeaderPrefix+name) - if err != nil { - return result, err - } - n, err := strconv.ParseUint(raw, 10, 64) - if err != nil || strconv.FormatUint(n, 10) != raw { - return result, fmt.Errorf("invalid canonical %s", name) - } - *out = n - } - names := map[string]bool{} - for _, value := range header.Values("Server-Timing") { - for _, item := range strings.Split(value, ",") { - names[strings.TrimSpace(strings.SplitN(item, ";", 2)[0])] = true - } - } - for _, name := range []string{"authentication-validate-stage", "persist", "receipt"} { - if !names[name] { - return result, fmt.Errorf("Server-Timing omits %s", name) - } - } - return result, nil -} -func parseAuthenticationAccounting(header http.Header) (WriteAccounting, uint64, error) { - var value WriteAccounting - encoded, err := oneHeader(header, authenticationHeaderPrefix+"Write-Accounting") - if err != nil { - return value, 0, err - } - if len(encoded) > base64.RawURLEncoding.EncodedLen(4096) { - return value, 0, fmt.Errorf("authentication accounting header exceeds limit") - } - raw, err := base64.RawURLEncoding.Strict().DecodeString(encoded) - if err != nil || len(raw) == 0 || len(raw) > 4096 { - return value, 0, fmt.Errorf("accounting is not bounded raw URL base64") - } - if err := rejectDuplicateJSONKeys(raw); err != nil { - return value, 0, err - } - if err := decodeStrict(raw, &value); err != nil { - return value, 0, err - } - if err := validateWriteAccounting(value); err != nil { - return value, 0, err - } - // Availability is explicit. A measurement consumer must require it; the - // writer correctness oracle intentionally makes no durable byte claim. - return value, uint64(len(raw)), nil -} diff --git a/internal/evaluation/gatewaytransport/authentication_test.go b/internal/evaluation/gatewaytransport/authentication_test.go deleted file mode 100644 index 5aaa2c0..0000000 --- a/internal/evaluation/gatewaytransport/authentication_test.go +++ /dev/null @@ -1,161 +0,0 @@ -package gatewaytransport_test - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "io" - "math" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-core/protocol" - cid "github.com/ipfs/go-cid" -) - -func TestEvaluationAuthenticationCandidateBindsSelectedRootAndCurrentWire(t *testing.T) { - candidate := validBootstrapCandidate(t).Candidate - for _, mode := range []string{"valid", "wrong-root", "old-profile", "duplicate-key", "cacheable"} { - t.Run(mode, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet || r.URL.Path != "/v1/evaluation/authentication/candidates/"+candidate.Root || r.Header.Get(gatewaytransport.InstanceTokenHeader) != testInstanceToken || r.Header.Get(gatewaytransport.BootstrapAuthorizationTokenHeader) != "" { - t.Errorf("unexpected request %s %s", r.Method, r.URL.Path) - } - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Cache-Control", "no-store") - value := candidate - switch mode { - case "wrong-root": - value.Root = mustRawCID(t, "wrong-root").String() - case "old-profile": - value.Profile = "malt.update-view/v2" - case "cacheable": - w.Header().Set("Cache-Control", "public, max-age=3600") - } - raw, err := json.Marshal(value) - if err != nil { - t.Error(err) - return - } - if mode == "duplicate-key" { - raw = bytes.Replace(raw, []byte(`"profile":`), []byte(`"profile":"malt.authentication/2","profile":`), 1) - } - _, _ = w.Write(raw) - })) - defer server.Close() - result, err := newEvaluationClient(t, server.URL, 0).AuthenticationCandidate(t.Context(), cid.MustParse(candidate.Root)) - if mode == "valid" { - if err != nil || result.Candidate.Root != candidate.Root || result.WireBytes == 0 { - t.Fatalf("result=%+v err=%v", result, err) - } - } else if err == nil { - t.Fatal("hostile candidate accepted") - } - }) - } -} - -func TestEvaluationAuthenticationBatchRequiresExactReceiptAndDiagnostics(t *testing.T) { - candidate := validBootstrapCandidate(t).Candidate - batch := protocol.AuthenticationBatch{Profile: protocol.AuthenticationBatchProfile, TransactionID: "typed-write-1", Base: candidate.Root, Root: candidate.Root, Candidates: []protocol.AuthenticationCandidate{candidate}} - digest, err := batch.Digest() - if err != nil { - t.Fatal(err) - } - baseReceipt := protocol.AuthenticationReceipt{Profile: protocol.AuthenticationReceiptProfile, TransactionID: batch.TransactionID, Base: batch.Base, Root: batch.Root, Digest: digest, DurableBoundary: gatewaytransport.AuthenticationDurableBoundary} - for _, mode := range []string{"valid", "retry", "receipt-root", "receipt-base", "receipt-id", "receipt-digest", "receipt-profile", "receipt-boundary", "duplicate-receipt", "header-boundary", "missing-phase", "duplicate-phase", "noncanonical-nanos", "missing-timing", "accounting-profile", "accounting-overflow", "duplicate-accounting", "cacheable"} { - t.Run(mode, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost || r.URL.Path != "/v1/evaluation/authentication/batches" || r.Header.Get(gatewaytransport.InstanceTokenHeader) != testInstanceToken || r.Header.Get(gatewaytransport.BootstrapAuthorizationTokenHeader) != "" { - t.Errorf("unexpected request %s %s", r.Method, r.URL.Path) - } - raw, err := io.ReadAll(r.Body) - if err != nil { - t.Error(err) - return - } - submitted, err := protocol.DecodeAuthenticationBatch(raw) - if err != nil { - t.Error(err) - return - } - if submittedDigest, err := submitted.Digest(); err != nil || submittedDigest != digest { - t.Errorf("changed batch digest=%s err=%v", submittedDigest, err) - } - receipt := baseReceipt - accounting := validWriteAccounting() - prefix := "X-Malt-Authentication-" - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Cache-Control", "no-store") - for k, v := range map[string]string{"Phase-Profile": gatewaytransport.AuthenticationPhaseProfile, "Publication": "separate", "Trust": "client-owned", "Durable-Boundary": gatewaytransport.AuthenticationDurableBoundary, "Validation-And-Stage-Nanos": "11", "Persist-Nanos": "12", "Receipt-Nanos": "13", "Idempotent": "false"} { - w.Header().Set(prefix+k, v) - } - w.Header().Set("Server-Timing", "authentication-validate-stage;dur=0.001, persist;dur=0.001, receipt;dur=0.001") - switch mode { - case "retry": - w.Header().Set(prefix+"Idempotent", "true") - case "receipt-root": - receipt.Root = mustRawCID(t, "root").String() - case "receipt-base": - receipt.Base = mustRawCID(t, "base").String() - case "receipt-id": - receipt.TransactionID = "another-write" - case "receipt-digest": - receipt.Digest = strings.Repeat("0", 64) - case "receipt-profile": - receipt.Profile = "malt.materialization-receipt/v2" - case "receipt-boundary": - receipt.DurableBoundary = "volatile" - case "header-boundary": - w.Header().Set(prefix+"Durable-Boundary", "volatile") - case "missing-phase": - w.Header().Del(prefix + "Persist-Nanos") - case "duplicate-phase": - w.Header().Add(prefix+"Persist-Nanos", "12") - case "noncanonical-nanos": - w.Header().Set(prefix+"Persist-Nanos", "012") - case "missing-timing": - w.Header().Del("Server-Timing") - case "accounting-profile": - accounting.Profile = "gateway.client-root-write-accounting/v2" - case "accounting-overflow": - accounting.Categories[0].AttemptedReplacementWrites = math.MaxUint64 - accounting.Categories[0].AttemptedWrites = 0 - case "cacheable": - w.Header().Set("Cache-Control", "public") - } - accountingJSON, err := json.Marshal(accounting) - if err != nil { - t.Error(err) - return - } - if mode == "duplicate-accounting" { - accountingJSON = bytes.Replace(accountingJSON, []byte(`"available":`), []byte(`"available":true,"available":`), 1) - } - w.Header().Set(prefix+"Write-Accounting", base64.RawURLEncoding.EncodeToString(accountingJSON)) - receiptJSON, err := json.Marshal(receipt) - if err != nil { - t.Error(err) - return - } - if mode == "duplicate-receipt" { - receiptJSON = bytes.Replace(receiptJSON, []byte(`"root":`), []byte(`"root":"ignored","root":`), 1) - } - w.WriteHeader(http.StatusCreated) - _, _ = w.Write(receiptJSON) - })) - defer server.Close() - result, err := newEvaluationClient(t, server.URL, 0).SubmitAuthenticationBatch(t.Context(), batch) - if mode == "valid" || mode == "retry" { - if err != nil || result.Receipt != baseReceipt || result.Gateway.ValidationAndStageNS != 11 || result.Gateway.PersistNS != 12 || result.Gateway.ReceiptNS != 13 || result.WriteAccountingWireBytes == 0 || result.RequestWireBytes == 0 || result.ResponseWireBytes == 0 || result.Idempotent != (mode == "retry") { - t.Fatalf("result=%+v err=%v", result, err) - } - } else if err == nil { - t.Fatal("hostile receipt/diagnostics accepted") - } - }) - } -} diff --git a/internal/evaluation/gatewaytransport/client.go b/internal/evaluation/gatewaytransport/client.go deleted file mode 100644 index 445df16..0000000 --- a/internal/evaluation/gatewaytransport/client.go +++ /dev/null @@ -1,562 +0,0 @@ -// Package gatewaytransport owns evaluator-only Gateway HTTP capabilities. -// Its instance client injects the disposable evaluation identity into every -// request, while bootstrap authorization uses a separate client so the -// controller-only secret is never combined with the instance token. -package gatewaytransport - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "math" - "net" - "net/http" - "net/url" - "path" - "strings" - "time" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - "github.com/dewebprotocol/malt-client/merkledag" - "github.com/dewebprotocol/malt-client/transport" - "github.com/dewebprotocol/malt-core/protocol" - cid "github.com/ipfs/go-cid" -) - -const ( - InstanceTokenHeader = "X-Malt-Evaluation-Instance-Token" - BootstrapAuthorizationTokenHeader = "X-Malt-Evaluation-Bootstrap-Authorization" - BootstrapProfile = "gateway.evaluation-authentication-bootstrap/0" - - defaultMaxJSONResponseBytes int64 = 96 << 20 - defaultMaxBlobResponseBytes int64 = 64 << 20 - defaultMaxErrorResponseBytes int64 = 1 << 20 - - WriteAccountingProfile = "gateway.authentication-write-accounting/0" - WriteByteMethod = "durable-kv-key-plus-value-bytes/v2" -) - -var writeCategories = []string{"arctable-arcset-records", "arctable-lineage-metadata", "root-version-metadata"} - -type Options struct { - BaseURL string - InstanceToken string - HTTPClient *http.Client - MaxJSONResponseBytes int64 - MaxBlobResponseBytes int64 - MaxErrorResponseBytes int64 -} - -// Client is an evaluator-only control and data-plane transport. Product code -// must use package transport instead. -type Client struct { - baseURL *url.URL - plainHTTP *http.Client - instanceHTTP *http.Client - maxJSONResponseBytes int64 - maxBlobResponseBytes int64 - maxErrorResponseBytes int64 -} - -// Health is the strict evaluation projection of Gateway health. The public -// transport health value deliberately omits these evaluator-only capabilities. -type Health struct { - Status string `json:"status"` - EvaluationInstanceToken string `json:"evaluation_instance_token,omitempty"` - KVBackend string `json:"kv_backend,omitempty"` - BlobBackend string `json:"blob_backend,omitempty"` - ArcTableMode string `json:"arctable_mode,omitempty"` - CommitmentProfile string `json:"default_commitment_backend,omitempty"` - CommitmentBackends string `json:"commitment_backends,omitempty"` - EvaluationCASWriteAccounting string `json:"evaluation_cas_write_accounting,omitempty"` - EvaluationCASWriteIsolation string `json:"evaluation_cas_write_isolation,omitempty"` - AuthenticationWriteAccounting string `json:"evaluation_authentication_write_accounting,omitempty"` - AuthenticationExactAcceptance string `json:"evaluation_authentication_exact_acceptance,omitempty"` - EvaluationAuthenticationBootstrap string `json:"evaluation_authentication_bootstrap,omitempty"` - EvaluationRQ3FlatPrefix string `json:"evaluation_rq3_flat_prefix,omitempty"` - EvaluationRQ3FlatPrefixLayout string `json:"evaluation_rq3_flat_prefix_layout,omitempty"` - EvaluationRQ3FlatPrefixStorageScope string `json:"evaluation_rq3_flat_prefix_storage_scope,omitempty"` - EvaluationRQ3FlatPrefixLookupIndex string `json:"evaluation_rq3_flat_prefix_lookup_index,omitempty"` - EvaluationRQ3FlatPrefixCommitmentTreatment string `json:"evaluation_rq3_flat_prefix_commitment_treatment,omitempty"` - EvaluationRQ3FlatPrefixFSKVMode string `json:"evaluation_rq3_flat_prefix_fskv_mode,omitempty"` - EvaluationRQ3FlatPrefixCheckpoint string `json:"evaluation_rq3_flat_prefix_checkpoint,omitempty"` - EvaluationRQ3FlatPrefixMaterializationCache string `json:"evaluation_rq3_flat_prefix_materialization_cache,omitempty"` -} - -type BootstrapObject struct { - OperationID string - Candidate protocol.AuthenticationCandidate -} -type BootstrapResult struct { - Root cid.Cid - ValidationAndStageNanos uint64 - PersistNanos uint64 - WriteAccounting WriteAccounting -} - -// RawCASGetter adapts the evaluator-only raw endpoint to merkledag.BlockGetter. -// The Merkle DAG verifier, not this transport, binds returned bytes to the CID. -type RawCASGetter struct { - Client *Client -} - -func (g RawCASGetter) Get(ctx context.Context, key cid.Cid) ([]byte, error) { - if g.Client == nil { - return nil, fmt.Errorf("evaluation raw CAS client is nil") - } - return g.Client.GetRawCAS(ctx, key) -} - -func New(options Options) (*Client, error) { - baseURL, err := parseBaseURL(options.BaseURL) - if err != nil { - return nil, err - } - if !canonicalLowerSHA256(options.InstanceToken) { - return nil, fmt.Errorf("evaluation instance token must be a canonical SHA-256") - } - if baseURL.Scheme != "https" && !(baseURL.Scheme == "http" && isLoopbackHost(baseURL.Hostname())) { - return nil, fmt.Errorf("evaluation instance token requires HTTPS or a loopback HTTP Gateway origin") - } - maxJSON, err := responseLimit(options.MaxJSONResponseBytes, defaultMaxJSONResponseBytes, "JSON") - if err != nil { - return nil, err - } - maxBlob, err := responseLimit(options.MaxBlobResponseBytes, defaultMaxBlobResponseBytes, "blob") - if err != nil { - return nil, err - } - maxError, err := responseLimit(options.MaxErrorResponseBytes, defaultMaxErrorResponseBytes, "error") - if err != nil { - return nil, err - } - baseHTTP := options.HTTPClient - if baseHTTP == nil { - baseHTTP = &http.Client{Timeout: 5 * time.Minute} - } - plainHTTP := cloneWithoutRedirects(baseHTTP, "evaluation Gateway") - instanceHTTP := cloneWithoutRedirects(baseHTTP, "evaluation instance") - roundTripper := instanceHTTP.Transport - if roundTripper == nil { - roundTripper = http.DefaultTransport - } - instanceHTTP.Transport = instanceTokenTransport{base: roundTripper, token: options.InstanceToken, allowedBase: baseURL} - return &Client{ - baseURL: baseURL, plainHTTP: plainHTTP, instanceHTTP: instanceHTTP, - maxJSONResponseBytes: maxJSON, maxBlobResponseBytes: maxBlob, maxErrorResponseBytes: maxError, - }, nil -} - -// InstanceHTTPClient returns a copy of the no-redirect HTTP client that injects -// the evaluation instance token. Evaluation adapters pass it to the ordinary -// untrusted transport so current authentication and CAS requests are bound -// to the same disposable Gateway instance. -func (c *Client) InstanceHTTPClient() *http.Client { - if c == nil || c.instanceHTTP == nil { - return nil - } - copyClient := *c.instanceHTTP - return ©Client -} - -func (c *Client) Health(ctx context.Context) (*Health, error) { - request, err := http.NewRequestWithContext(ctx, http.MethodGet, c.endpoint("/healthz"), nil) - if err != nil { - return nil, err - } - var health Health - if err := c.executeJSON(c.instanceHTTP, request, &health); err != nil { - return nil, err - } - return &health, nil -} - -// BootstrapEvaluationObject installs one first-campaign typed candidate with -// the controller-only bootstrap secret. It intentionally uses plainHTTP, so -// the instance token is not attached to this distinct authorization boundary. -func (c *Client) BootstrapEvaluationObject(ctx context.Context, bootstrapAuthorizationToken string, value BootstrapObject) (BootstrapResult, error) { - if !canonicalLowerSHA256(bootstrapAuthorizationToken) { - return BootstrapResult{}, fmt.Errorf("evaluation bootstrap authorization token must be a canonical SHA-256") - } - if value.OperationID == "" || value.Candidate.Previous != "" { - return BootstrapResult{}, fmt.Errorf("bootstrap requires a fresh candidate and operation ID") - } - if err := value.Candidate.Validate(); err != nil { - return BootstrapResult{}, err - } - expectedRoot, err := cid.Decode(value.Candidate.Root) - if err != nil { - return BootstrapResult{}, err - } - requestBody := struct { - Profile string `json:"profile"` - OperationID string `json:"operation_id"` - Candidate protocol.AuthenticationCandidate `json:"candidate"` - }{BootstrapProfile, value.OperationID, value.Candidate} - encoded, err := json.Marshal(requestBody) - if err != nil { - return BootstrapResult{}, err - } - request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint("/v1/evaluation/authentication/bootstrap"), bytes.NewReader(encoded)) - if err != nil { - return BootstrapResult{}, err - } - request.Header.Set("Content-Type", "application/json") - request.Header.Set(BootstrapAuthorizationTokenHeader, bootstrapAuthorizationToken) - response, err := c.plainHTTP.Do(request) - if err != nil { - return BootstrapResult{}, err - } - defer response.Body.Close() - if response.StatusCode < 200 || response.StatusCode >= 300 { - return BootstrapResult{}, c.responseError(response) - } - if err := requireJSONNoStore(response); err != nil { - return BootstrapResult{}, err - } - raw, err := readBounded(response.Body, c.maxJSONResponseBytes, "Gateway evaluation bootstrap response") - if err != nil { - return BootstrapResult{}, err - } - if err := rejectDuplicateJSONKeys(raw); err != nil { - return BootstrapResult{}, fmt.Errorf("Gateway evaluation bootstrap response: %w", err) - } - var wire struct { - Profile string `json:"profile"` - Root string `json:"root"` - ValidationAndStageNanos uint64 `json:"validation_and_stage_nanos"` - PersistNanos uint64 `json:"persist_nanos"` - WriteAccounting WriteAccounting `json:"write_accounting"` - } - if err := decodeStrict(raw, &wire); err != nil { - return BootstrapResult{}, fmt.Errorf("decode Gateway evaluation bootstrap response: %w", err) - } - root, err := cid.Parse(wire.Root) - if err != nil || !root.Equals(expectedRoot) || wire.Profile != BootstrapProfile { - return BootstrapResult{}, fmt.Errorf("Gateway evaluation bootstrap returned a mismatched exact Root") - } - if err := validateWriteAccounting(wire.WriteAccounting); err != nil || !wire.WriteAccounting.Available || - strings.TrimSpace(wire.WriteAccounting.UnavailableReason) != "" { - return BootstrapResult{}, fmt.Errorf("Gateway evaluation bootstrap returned invalid exact write accounting") - } - return BootstrapResult{ - Root: root, ValidationAndStageNanos: wire.ValidationAndStageNanos, PersistNanos: wire.PersistNanos, WriteAccounting: wire.WriteAccounting, - }, nil -} - -// GetRawCAS returns bounded bytes without hashing them. Only the Direct-CAS -// evaluator uses this method so its local verifier can own and time the single -// CID hash. -func (c *Client) GetRawCAS(ctx context.Context, key cid.Cid) ([]byte, error) { - if !key.Defined() { - return nil, fmt.Errorf("Gateway CAS key is undefined") - } - request, err := http.NewRequestWithContext(ctx, http.MethodGet, c.endpoint("/v1/cas/"+url.PathEscape(key.String())), nil) - if err != nil { - return nil, err - } - response, err := c.instanceHTTP.Do(request) - if err != nil { - return nil, err - } - defer response.Body.Close() - if response.StatusCode < 200 || response.StatusCode >= 300 { - responseErr := c.responseError(response) - if response.StatusCode == http.StatusNotFound { - return nil, fmt.Errorf("%w: %w", clientcas.ErrNotFound, responseErr) - } - return nil, responseErr - } - return readBounded(response.Body, c.maxBlobResponseBytes, "Gateway evaluation CAS body") -} - -// ReadCAR invokes the evaluator-only selective-CAR route. Callers must pass the -// returned bytes to merkledag.VerifyMerkleDAGCARRead before using any result. -func (c *Client) ReadCAR(ctx context.Context, encodedRequest []byte) ([]byte, error) { - request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint("/v1/compat/merkledag/car/read"), bytes.NewReader(encodedRequest)) - if err != nil { - return nil, err - } - request.Header.Set("Content-Type", "application/json") - response, err := c.instanceHTTP.Do(request) - if err != nil { - return nil, err - } - defer response.Body.Close() - if response.StatusCode < 200 || response.StatusCode >= 300 { - return nil, c.responseError(response) - } - if got := response.Header.Get("Content-Type"); got != merkledag.MerkleDAGCARReadMediaType { - return nil, fmt.Errorf("unsupported Gateway Merkle-DAG CAR content type %q", got) - } - return readBounded(response.Body, c.maxBlobResponseBytes, "Gateway Merkle-DAG CAR response") -} - -type instanceTokenTransport struct { - base http.RoundTripper - token string - allowedBase *url.URL -} - -func (t instanceTokenTransport) RoundTrip(request *http.Request) (*http.Response, error) { - if !withinBaseURL(request.URL, t.allowedBase) || - request.Host != "" && !strings.EqualFold(request.Host, t.allowedBase.Host) { - return nil, fmt.Errorf("refusing to send evaluation instance token outside configured Gateway base URL") - } - copyRequest := request.Clone(request.Context()) - copyRequest.Header = request.Header.Clone() - copyRequest.Header.Set(InstanceTokenHeader, t.token) - return t.base.RoundTrip(copyRequest) -} - -func cloneWithoutRedirects(source *http.Client, label string) *http.Client { - copyClient := *source - copyClient.CheckRedirect = func(next *http.Request, _ []*http.Request) error { - return fmt.Errorf("refusing %s redirect to %s", label, next.URL.Redacted()) - } - return ©Client -} - -func parseBaseURL(raw string) (*url.URL, error) { - parsed, err := url.Parse(strings.TrimRight(strings.TrimSpace(raw), "/")) - if err != nil || parsed.Scheme != "http" && parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || - parsed.Opaque != "" || parsed.RawPath != "" || parsed.RawQuery != "" || parsed.Fragment != "" || - parsed.Path != "" && (parsed.Path[0] != '/' || path.Clean(parsed.Path) != parsed.Path) { - return nil, fmt.Errorf("evaluation Gateway base URL must be an absolute HTTP(S) URL without query or fragment") - } - return parsed, nil -} - -func withinBaseURL(candidate, base *url.URL) bool { - if candidate == nil || base == nil || !strings.EqualFold(candidate.Scheme, base.Scheme) || - !strings.EqualFold(candidate.Host, base.Host) || candidate.User != nil || - candidate.Opaque != "" || candidate.RawPath != "" || - candidate.Path != "" && (candidate.Path[0] != '/' || path.Clean(candidate.Path) != candidate.Path) { - return false - } - basePath := strings.TrimSuffix(base.Path, "/") - return basePath == "" || candidate.Path == basePath || strings.HasPrefix(candidate.Path, basePath+"/") -} - -func (c *Client) endpoint(route string) string { - copyURL := *c.baseURL - copyURL.Path = path.Join(copyURL.Path, route) - if strings.HasSuffix(route, "/") && !strings.HasSuffix(copyURL.Path, "/") { - copyURL.Path += "/" - } - return copyURL.String() -} - -func (c *Client) executeJSON(client *http.Client, request *http.Request, output any) error { - response, err := client.Do(request) - if err != nil { - return err - } - defer response.Body.Close() - if response.StatusCode < 200 || response.StatusCode >= 300 { - return c.responseError(response) - } - data, err := readBounded(response.Body, c.maxJSONResponseBytes, "Gateway evaluation JSON response") - if err != nil { - return err - } - if err := rejectDuplicateJSONKeys(data); err != nil { - return fmt.Errorf("decode Gateway evaluation JSON response: %w", err) - } - decoder := json.NewDecoder(bytes.NewReader(data)) - if err := decoder.Decode(output); err != nil { - return fmt.Errorf("decode Gateway evaluation JSON response: %w", err) - } - if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { - if err == nil { - return fmt.Errorf("decode Gateway evaluation JSON response: trailing JSON") - } - return fmt.Errorf("decode Gateway evaluation JSON response: %w", err) - } - return nil -} - -func (c *Client) responseError(response *http.Response) error { - data, err := readBounded(response.Body, c.maxErrorResponseBytes, "Gateway evaluation error response") - if err != nil { - return err - } - var body struct { - Error string `json:"error,omitempty"` - Message string `json:"message,omitempty"` - } - message := strings.TrimSpace(string(data)) - if json.Unmarshal(data, &body) == nil { - if body.Message != "" { - message = body.Message - } else if body.Error != "" { - message = body.Error - } - } - if message == "" { - message = http.StatusText(response.StatusCode) - } - return &transport.Error{StatusCode: response.StatusCode, Message: message} -} - -func requireJSONNoStore(response *http.Response) error { - mediaType := strings.TrimSpace(strings.Split(response.Header.Get("Content-Type"), ";")[0]) - if mediaType != "application/json" { - return fmt.Errorf("Gateway evaluation bootstrap response has unsupported Content-Type %q", response.Header.Get("Content-Type")) - } - for _, directive := range strings.Split(strings.ToLower(response.Header.Get("Cache-Control")), ",") { - if strings.TrimSpace(directive) == "no-store" { - return nil - } - } - return fmt.Errorf("Gateway evaluation bootstrap response is missing Cache-Control: no-store") -} - -func validateWriteAccounting(accounting WriteAccounting) error { - if accounting.Profile != WriteAccountingProfile || accounting.ByteMethod != WriteByteMethod { - return fmt.Errorf("Gateway authentication write accounting has unsupported profile/method") - } - if !accounting.Available { - if accounting.UnavailableReason == "" || accounting.ObjectLedgerSHA256 != "" || len(accounting.Categories) != 0 { - return fmt.Errorf("unavailable Gateway authentication write accounting carries measurements") - } - return nil - } - if accounting.UnavailableReason != "" || !canonicalLowerSHA256(accounting.ObjectLedgerSHA256) || - len(accounting.Categories) != len(writeCategories) { - return fmt.Errorf("available Gateway authentication write accounting is incomplete") - } - for index, category := range accounting.Categories { - if category.Category != writeCategories[index] || !sumAmounts(category.AttemptedWrites, category.AttemptedNewWrites, category.AttemptedReplacementWrites, category.AttemptedSameValueWrites, category.AttemptedDeleteWrites) || - !sumAmounts(category.AttemptedBytes, category.AttemptedNewBytes, category.AttemptedReplacementBytes, category.AttemptedSameValueBytes, category.AttemptedDeleteBytes) || category.GrossNewBytes > math.MaxInt64 || category.ReclaimedBytes > math.MaxInt64 || category.NetBytes != int64(category.GrossNewBytes)-int64(category.ReclaimedBytes) || - !sumAmounts(category.NewlyPersistedWrites, category.NewWrites, category.ReplacedWrites) || - !sumAmounts(category.GrossNewBytes, category.NewBytes, category.ReplacementNewBytes) || - !sumAmounts(category.ReclaimedBytes, category.ReplacementReclaimedBytes, category.DeletedReclaimedBytes) { - return fmt.Errorf("Gateway authentication write accounting category %d is inconsistent", index) - } - } - return nil -} - -func canonicalLowerSHA256(value string) bool { - if len(value) != 64 { - return false - } - for _, character := range value { - if (character < '0' || character > '9') && (character < 'a' || character > 'f') { - return false - } - } - return true -} - -func isLoopbackHost(host string) bool { - if strings.EqualFold(strings.TrimSpace(host), "localhost") { - return true - } - ip := net.ParseIP(host) - return ip != nil && ip.IsLoopback() -} - -func responseLimit(value, fallback int64, kind string) (int64, error) { - if value < 0 { - return 0, fmt.Errorf("maximum %s response size must not be negative", kind) - } - if value == 0 { - return fallback, nil - } - return value, nil -} - -func readBounded(reader io.Reader, limit int64, description string) ([]byte, error) { - if limit <= 0 { - return nil, fmt.Errorf("%s limit is invalid", description) - } - readLimit := limit - if limit < int64(^uint64(0)>>1) { - readLimit++ - } - data, err := io.ReadAll(io.LimitReader(reader, readLimit)) - if err != nil { - return nil, err - } - if int64(len(data)) > limit { - return nil, fmt.Errorf("%s exceeds %d-byte limit", description, limit) - } - return data, nil -} - -func decodeStrict(data []byte, output any) error { - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.DisallowUnknownFields() - if err := decoder.Decode(output); err != nil { - return err - } - if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { - if err == nil { - return fmt.Errorf("trailing JSON") - } - return err - } - return nil -} - -func rejectDuplicateJSONKeys(data []byte) error { - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.UseNumber() - if err := scanJSONValue(decoder, "$"); err != nil { - return err - } - if token, err := decoder.Token(); !errors.Is(err, io.EOF) { - if err == nil { - return fmt.Errorf("trailing token %v", token) - } - return err - } - return nil -} - -func scanJSONValue(decoder *json.Decoder, location string) error { - token, err := decoder.Token() - if err != nil { - return err - } - delimiter, ok := token.(json.Delim) - if !ok { - return nil - } - switch delimiter { - case '{': - seen := make(map[string]struct{}) - for decoder.More() { - rawKey, err := decoder.Token() - if err != nil { - return err - } - key, ok := rawKey.(string) - if !ok { - return fmt.Errorf("%s has a non-string object key", location) - } - if _, duplicate := seen[key]; duplicate { - return fmt.Errorf("%s has duplicate key %q", location, key) - } - seen[key] = struct{}{} - if err := scanJSONValue(decoder, location+"."+key); err != nil { - return err - } - } - case '[': - for index := 0; decoder.More(); index++ { - if err := scanJSONValue(decoder, fmt.Sprintf("%s[%d]", location, index)); err != nil { - return err - } - } - default: - return fmt.Errorf("%s has unexpected delimiter %q", location, delimiter) - } - _, err = decoder.Token() - return err -} diff --git a/internal/evaluation/gatewaytransport/client_test.go b/internal/evaluation/gatewaytransport/client_test.go deleted file mode 100644 index ba75dff..0000000 --- a/internal/evaluation/gatewaytransport/client_test.go +++ /dev/null @@ -1,406 +0,0 @@ -package gatewaytransport_test - -import ( - "encoding/json" - "errors" - "io" - "net/http" - "net/http/httptest" - "strings" - "sync/atomic" - "testing" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-client/merkledag" - "github.com/dewebprotocol/malt-client/transport" - "github.com/dewebprotocol/malt-core/auth/commitment/kzg" - "github.com/dewebprotocol/malt-core/derivation" - "github.com/dewebprotocol/malt-core/engine" - "github.com/dewebprotocol/malt-core/maltcid" - "github.com/dewebprotocol/malt-core/protocol" - "github.com/dewebprotocol/malt-core/sdk/authentication" - cid "github.com/ipfs/go-cid" -) - -const testInstanceToken = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - -func TestNewRequiresCanonicalTokenAndProtectedOrigin(t *testing.T) { - for _, test := range []struct { - name string - baseURL string - token string - want string - }{ - {name: "invalid token", baseURL: "https://gateway.example", token: "not-a-token", want: "canonical SHA-256"}, - {name: "plaintext remote", baseURL: "http://192.0.2.1:8080", token: testInstanceToken, want: "HTTPS or a loopback"}, - {name: "query is forbidden", baseURL: "https://gateway.example/prefix?secret=true", token: testInstanceToken, want: "without query"}, - } { - t.Run(test.name, func(t *testing.T) { - if _, err := gatewaytransport.New(gatewaytransport.Options{BaseURL: test.baseURL, InstanceToken: test.token}); err == nil || - !strings.Contains(err.Error(), test.want) { - t.Fatalf("New error = %v, want substring %q", err, test.want) - } - }) - } -} - -func TestInstanceClientSupportsGatewayPrefixAndRejectsOtherTargets(t *testing.T) { - var prefixRequests atomic.Int32 - server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - if request.URL.Path != "/gateway/healthz" { - http.NotFound(response, request) - return - } - prefixRequests.Add(1) - if request.Header.Get(gatewaytransport.InstanceTokenHeader) != testInstanceToken { - t.Errorf("instance token = %q", request.Header.Get(gatewaytransport.InstanceTokenHeader)) - } - _, _ = io.WriteString(response, `{"status":"ok"}`) - })) - defer server.Close() - evaluation := newEvaluationClient(t, server.URL+"/gateway", 0) - if _, err := evaluation.Health(t.Context()); err != nil { - t.Fatal(err) - } - - other := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - if request.Header.Get(gatewaytransport.InstanceTokenHeader) != "" { - t.Errorf("other origin received instance token") - } - response.WriteHeader(http.StatusNoContent) - })) - defer other.Close() - request, err := http.NewRequestWithContext(t.Context(), http.MethodGet, other.URL, nil) - if err != nil { - t.Fatal(err) - } - if _, err := evaluation.InstanceHTTPClient().Do(request); err == nil || !strings.Contains(err.Error(), "outside configured Gateway base URL") { - t.Fatalf("cross-origin request error = %v", err) - } - if prefixRequests.Load() != 1 { - t.Fatalf("prefix requests = %d", prefixRequests.Load()) - } -} - -func TestInstanceClientRejectsPrefixTraversalAndHostOverride(t *testing.T) { - var received atomic.Int32 - server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - received.Add(1) - if request.Header.Get(gatewaytransport.InstanceTokenHeader) != "" { - t.Errorf("escaped request leaked instance token") - } - response.WriteHeader(http.StatusNoContent) - })) - defer server.Close() - evaluation := newEvaluationClient(t, server.URL+"/gateway", 0) - - tests := []struct { - name string - url string - host string - }{ - {name: "dot traversal", url: server.URL + "/gateway/../outside"}, - {name: "encoded traversal", url: server.URL + "/gateway/%2e%2e/outside"}, - {name: "encoded slash", url: server.URL + "/gateway/%2Foutside"}, - {name: "duplicate slash", url: server.URL + "/gateway//outside"}, - {name: "host override", url: server.URL + "/gateway/healthz", host: "other.example"}, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - request, err := http.NewRequestWithContext(t.Context(), http.MethodGet, test.url, nil) - if err != nil { - t.Fatal(err) - } - request.Host = test.host - if _, err := evaluation.InstanceHTTPClient().Do(request); err == nil || - !strings.Contains(err.Error(), "outside configured Gateway base URL") { - t.Fatalf("request error = %v", err) - } - }) - } - if received.Load() != 0 { - t.Fatalf("prefix escape reached Gateway %d times", received.Load()) - } -} - -func TestInstanceClientAndHealthBindEveryRequestToToken(t *testing.T) { - var requests atomic.Int32 - server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - requests.Add(1) - if got := request.Header.Get(gatewaytransport.InstanceTokenHeader); got != testInstanceToken { - t.Errorf("instance token = %q", got) - } - switch request.URL.Path { - case "/healthz": - _, _ = io.WriteString(response, `{"status":"ok","evaluation_instance_token":"`+testInstanceToken+`"}`) - case "/v1/client-roots": - response.WriteHeader(http.StatusNoContent) - default: - http.NotFound(response, request) - } - })) - defer server.Close() - - evaluation := newEvaluationClient(t, server.URL, 0) - health, err := evaluation.Health(t.Context()) - if err != nil { - t.Fatal(err) - } - if health.Status != "ok" || health.EvaluationInstanceToken != testInstanceToken { - t.Fatalf("health = %#v", health) - } - request, err := http.NewRequestWithContext(t.Context(), http.MethodPost, server.URL+"/v1/client-roots", nil) - if err != nil { - t.Fatal(err) - } - response, err := evaluation.InstanceHTTPClient().Do(request) - if err != nil { - t.Fatal(err) - } - _ = response.Body.Close() - if response.StatusCode != http.StatusNoContent || requests.Load() != 2 { - t.Fatalf("generic instance request = status %d requests %d", response.StatusCode, requests.Load()) - } -} - -func TestHealthRejectsDuplicateOrTrailingFields(t *testing.T) { - for _, body := range []string{ - `{"status":"ok","status":"bad"}`, - `{"status":"ok"}{"status":"bad"}`, - } { - server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) { - _, _ = io.WriteString(response, body) - })) - evaluation := newEvaluationClient(t, server.URL, 0) - _, err := evaluation.Health(t.Context()) - server.Close() - if err == nil { - t.Fatalf("Health accepted %s", body) - } - } -} - -func TestHealthAllowsUnrelatedGatewayCapabilities(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) { - _, _ = io.WriteString(response, `{"service":"malt-gateway","status":"ok","future_capability":"value"}`) - })) - defer server.Close() - health, err := newEvaluationClient(t, server.URL, 0).Health(t.Context()) - if err != nil || health.Status != "ok" { - t.Fatalf("Health = %#v err=%v", health, err) - } -} - -func TestBootstrapUsesDistinctAuthorizationAndBindsRootAccounting(t *testing.T) { - object := validBootstrapCandidate(t) - bootstrapToken := strings.Repeat("b", 64) - server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - if request.Method != http.MethodPost || request.URL.Path != "/v1/evaluation/authentication/bootstrap" { - t.Fatalf("request = %s %s", request.Method, request.URL.Path) - } - if request.Header.Get(gatewaytransport.BootstrapAuthorizationTokenHeader) != bootstrapToken || - request.Header.Get(gatewaytransport.InstanceTokenHeader) != "" || - request.Header.Get("Content-Type") != "application/json" { - t.Fatalf("request headers = %#v", request.Header) - } - var body struct { - Profile string `json:"profile"` - OperationID string `json:"operation_id"` - Candidate protocol.AuthenticationCandidate `json:"candidate"` - } - decoder := json.NewDecoder(request.Body) - decoder.DisallowUnknownFields() - if err := decoder.Decode(&body); err != nil { - t.Fatal(err) - } - if body.Profile != gatewaytransport.BootstrapProfile || body.OperationID != object.OperationID || body.Candidate.Root != object.Candidate.Root || len(body.Candidate.State.Entries) != 1 || string(body.Candidate.State.Entries[0].Label) != "payload" { - t.Fatalf("bootstrap request = %#v", body) - } - response.Header().Set("Content-Type", "application/json") - response.Header().Set("Cache-Control", "private, no-store") - _ = json.NewEncoder(response).Encode(map[string]any{ - "profile": gatewaytransport.BootstrapProfile, "root": object.Candidate.Root, - "validation_and_stage_nanos": uint64(11), "persist_nanos": uint64(22), - "write_accounting": validWriteAccounting(), - }) - })) - defer server.Close() - - result, err := newEvaluationClient(t, server.URL, 0).BootstrapEvaluationObject(t.Context(), bootstrapToken, object) - if err != nil { - t.Fatal(err) - } - if !result.Root.Equals(cid.MustParse(object.Candidate.Root)) || result.ValidationAndStageNanos != 11 || result.PersistNanos != 22 || - !result.WriteAccounting.Available { - t.Fatalf("bootstrap result = %#v", result) - } -} - -func TestBootstrapRejectsRedirectWithoutLeakingAuthorization(t *testing.T) { - object := validBootstrapCandidate(t) - bootstrapToken := strings.Repeat("b", 64) - var targetRequests atomic.Int32 - target := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - targetRequests.Add(1) - if got := request.Header.Get(gatewaytransport.BootstrapAuthorizationTokenHeader); got != "" { - t.Errorf("redirect target received bootstrap authorization %q", got) - } - response.WriteHeader(http.StatusNoContent) - })) - defer target.Close() - source := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - if request.Header.Get(gatewaytransport.BootstrapAuthorizationTokenHeader) != bootstrapToken { - t.Errorf("source did not receive bootstrap authorization") - } - http.Redirect(response, request, target.URL, http.StatusTemporaryRedirect) - })) - defer source.Close() - - _, err := newEvaluationClient(t, source.URL, 0).BootstrapEvaluationObject(t.Context(), bootstrapToken, object) - if err == nil || !strings.Contains(err.Error(), "refusing evaluation Gateway redirect") { - t.Fatalf("bootstrap redirect error = %v", err) - } - if targetRequests.Load() != 0 { - t.Fatalf("redirect target received %d requests", targetRequests.Load()) - } -} - -func TestRawCASDefersCIDVerificationButKeepsTokenAndBounds(t *testing.T) { - requested := mustRawCID(t, "expected") - hostile := []byte("wrong") - server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - if request.Method != http.MethodGet || request.URL.Path != "/v1/cas/"+requested.String() { - http.NotFound(response, request) - return - } - if request.Header.Get(gatewaytransport.InstanceTokenHeader) != testInstanceToken { - http.Error(response, "missing token", http.StatusUnauthorized) - return - } - _, _ = response.Write(hostile) - })) - defer server.Close() - - raw, err := newEvaluationClient(t, server.URL, int64(len(hostile))).GetRawCAS(t.Context(), requested) - if err != nil || string(raw) != string(hostile) { - t.Fatalf("raw CAS = %q err=%v", raw, err) - } - _, err = newEvaluationClient(t, server.URL, int64(len(hostile)-1)).GetRawCAS(t.Context(), requested) - if err == nil || !strings.Contains(err.Error(), "exceeds") { - t.Fatalf("bounded raw CAS error = %v", err) - } -} - -func TestRawCASClassifiesOnlyNotFound(t *testing.T) { - requested := mustRawCID(t, "missing") - server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) { - http.Error(response, "missing", http.StatusNotFound) - })) - defer server.Close() - - _, err := newEvaluationClient(t, server.URL, 0).GetRawCAS(t.Context(), requested) - if !errors.Is(err, clientcas.ErrNotFound) { - t.Fatalf("raw CAS error = %v, want cas.ErrNotFound", err) - } - var apiErr *transport.Error - if !errors.As(err, &apiErr) || apiErr.StatusCode != http.StatusNotFound { - t.Fatalf("raw CAS error = %T %v, want structured 404", err, err) - } -} - -func TestReadCARRequiresTokenExactMediaTypeAndBound(t *testing.T) { - const requestBody = `{"profile":"merkledag.read/v0alpha1"}` - responseBody := []byte("untrusted CAR bytes") - server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - if request.Method != http.MethodPost || request.URL.Path != "/v1/compat/merkledag/car/read" || - request.Header.Get("Content-Type") != "application/json" || - request.Header.Get(gatewaytransport.InstanceTokenHeader) != testInstanceToken { - http.Error(response, "bad request", http.StatusBadRequest) - return - } - body, _ := io.ReadAll(request.Body) - if string(body) != requestBody { - t.Errorf("CAR request = %q", body) - } - response.Header().Set("Content-Type", merkledag.MerkleDAGCARReadMediaType) - _, _ = response.Write(responseBody) - })) - defer server.Close() - - got, err := newEvaluationClient(t, server.URL, int64(len(responseBody))).ReadCAR(t.Context(), []byte(requestBody)) - if err != nil || string(got) != string(responseBody) { - t.Fatalf("CAR response = %q err=%v", got, err) - } - _, err = newEvaluationClient(t, server.URL, int64(len(responseBody)-1)).ReadCAR(t.Context(), []byte(requestBody)) - if err == nil || !strings.Contains(err.Error(), "exceeds") { - t.Fatalf("bounded CAR error = %v", err) - } -} - -func TestReadCARRejectsWrongMediaType(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) { - response.Header().Set("Content-Type", "application/octet-stream") - _, _ = response.Write([]byte("not a CAR")) - })) - defer server.Close() - _, err := newEvaluationClient(t, server.URL, 0).ReadCAR(t.Context(), []byte(`{}`)) - if err == nil || !strings.Contains(err.Error(), "content type") { - t.Fatalf("wrong media type error = %v", err) - } -} - -func newEvaluationClient(t *testing.T, baseURL string, maxBlob int64) *gatewaytransport.Client { - t.Helper() - client, err := gatewaytransport.New(gatewaytransport.Options{ - BaseURL: baseURL, InstanceToken: testInstanceToken, MaxBlobResponseBytes: maxBlob, - }) - if err != nil { - t.Fatal(err) - } - return client -} - -func validBootstrapCandidate(t *testing.T) gatewaytransport.BootstrapObject { - t.Helper() - scheme, err := kzg.NewScheme() - if err != nil { - t.Fatal(err) - } - profiles := engine.NewRegistry() - if err := profiles.Register(scheme); err != nil { - t.Fatal(err) - } - e := engine.New(profiles) - candidate, err := authentication.Prepare(t.Context(), e, engine.State{Descriptor: maltcid.RootDescriptor{Layout: maltcid.Prefix, DerivationProfile: uint8(derivation.SHA256), Profile: maltcid.KZG4096}, Entries: []engine.Entry{{Label: []byte("payload"), Target: mustRawCID(t, "bootstrap-target")}}}) - if err != nil { - t.Fatal(err) - } - return gatewaytransport.BootstrapObject{OperationID: "bootstrap-1", Candidate: candidate} -} - -func validWriteAccounting() gatewaytransport.WriteAccounting { - categories := make([]gatewaytransport.WriteCategoryAccounting, 0, 3) - for _, category := range []string{"arctable-arcset-records", "arctable-lineage-metadata", "root-version-metadata"} { - categories = append(categories, gatewaytransport.WriteCategoryAccounting{ - Category: category, AttemptedWrites: 1, AttemptedBytes: 2, - AttemptedNewWrites: 1, AttemptedNewBytes: 2, - NewlyPersistedWrites: 1, GrossNewBytes: 2, - NewWrites: 1, NewBytes: 2, NetBytes: 2, - }) - } - return gatewaytransport.WriteAccounting{ - Profile: "gateway.authentication-write-accounting/0", Available: true, - ByteMethod: "durable-kv-key-plus-value-bytes/v2", - ObjectLedgerSHA256: strings.Repeat("c", 64), Categories: categories, - } -} - -func mustRawCID(t *testing.T, value string) cid.Cid { - t.Helper() - key, err := clientcas.CIDForBlock(clientcas.Block{Data: []byte(value), Codec: cid.Raw}) - if err != nil { - t.Fatal(err) - } - return key -} diff --git a/internal/evaluation/gatewaytransport/flatmap_test.go b/internal/evaluation/gatewaytransport/flatmap_test.go deleted file mode 100644 index 5a86f5f..0000000 --- a/internal/evaluation/gatewaytransport/flatmap_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package gatewaytransport_test - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - cid "github.com/ipfs/go-cid" -) - -func TestApplyEvaluationFlatPrefixUsesSecretOnlyAndRequiresArcSetOnlyAccounting(t *testing.T) { - root := cid.MustParse(validBootstrapCandidate(t).Candidate.Root) - target := mustRawCID(t, "flat-target") - secret := strings.Repeat("b", 64) - server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - if request.Method != http.MethodPost || request.URL.Path != "/v1/evaluation/rq3/flat-prefix" { - t.Fatalf("request = %s %s", request.Method, request.URL.Path) - } - if request.Header.Get(gatewaytransport.BootstrapAuthorizationTokenHeader) != secret || - request.Header.Get(gatewaytransport.InstanceTokenHeader) != "" { - t.Fatalf("flat-prefix authorization headers = %#v", request.Header) - } - var body struct { - Profile string `json:"profile"` - OperationID string `json:"operation_id"` - Initial bool `json:"initial"` - Changes []struct { - Label []byte `json:"label"` - After string `json:"after"` - } `json:"changes"` - } - decoder := json.NewDecoder(request.Body) - decoder.DisallowUnknownFields() - if err := decoder.Decode(&body); err != nil { - t.Fatal(err) - } - if body.Profile != gatewaytransport.FlatPrefixProfile || body.OperationID != "snapshot" || - !body.Initial || len(body.Changes) != 1 || string(body.Changes[0].Label) != "rq3/file-sha256/abc" || - body.Changes[0].After != target.String() { - t.Fatalf("flat-prefix request = %#v", body) - } - response.Header().Set("Content-Type", "application/json") - response.Header().Set("Cache-Control", "private, no-store") - _ = json.NewEncoder(response).Encode(map[string]any{ - "profile": gatewaytransport.FlatPrefixProfile, "root": root.String(), - "validation_and_stage_nanos": uint64(11), "persist_nanos": uint64(13), - "write_accounting": flatWriteAccounting(), - }) - })) - defer server.Close() - - result, err := newEvaluationClient(t, server.URL, 0).ApplyEvaluationFlatPrefix(t.Context(), secret, gatewaytransport.FlatPrefixMutation{ - OperationID: "snapshot", Initial: true, - Changes: []gatewaytransport.FlatPrefixChange{{Label: []byte("rq3/file-sha256/abc"), After: target}}, - }) - if err != nil { - t.Fatal(err) - } - if !result.Root.Equals(root) || result.ValidationAndStageNanos != 11 || result.PersistNanos != 13 || - result.WriteAccounting.Categories[0].GrossNewBytes == 0 { - t.Fatalf("flat-prefix result = %#v", result) - } -} - -func flatWriteAccounting() gatewaytransport.WriteAccounting { - value := validWriteAccounting() - value.Categories[1] = gatewaytransport.WriteCategoryAccounting{Category: "arctable-lineage-metadata"} - value.Categories[2] = gatewaytransport.WriteCategoryAccounting{Category: "root-version-metadata"} - return value -} - -func TestApplyEvaluationFlatPrefixRejectsNonzeroDisabledCategoryCounter(t *testing.T) { - root := cid.MustParse(validBootstrapCandidate(t).Candidate.Root) - target := mustRawCID(t, "flat-target") - secret := strings.Repeat("c", 64) - accounting := flatWriteAccounting() - accounting.Categories[1].AttemptedWrites = 1 - accounting.Categories[1].AttemptedSameValueWrites = 1 - accounting.Categories[1].AttemptedBytes = 17 - accounting.Categories[1].AttemptedSameValueBytes = 17 - server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) { - response.Header().Set("Content-Type", "application/json") - response.Header().Set("Cache-Control", "no-store") - _ = json.NewEncoder(response).Encode(map[string]any{ - "profile": gatewaytransport.FlatPrefixProfile, "root": root.String(), - "validation_and_stage_nanos": uint64(1), "persist_nanos": uint64(1), "write_accounting": accounting, - }) - })) - defer server.Close() - - _, err := newEvaluationClient(t, server.URL, 0).ApplyEvaluationFlatPrefix(t.Context(), secret, gatewaytransport.FlatPrefixMutation{ - OperationID: "snapshot", Initial: true, - Changes: []gatewaytransport.FlatPrefixChange{{Label: []byte("rq3/file-sha256/abc"), After: target}}, - }) - if err == nil || !strings.Contains(err.Error(), "non-ArcSet metadata") { - t.Fatalf("disabled-category attempted same-value counter error = %v", err) - } -} diff --git a/internal/evaluation/gatewaytransport/flatprefix.go b/internal/evaluation/gatewaytransport/flatprefix.go deleted file mode 100644 index 83c22fb..0000000 --- a/internal/evaluation/gatewaytransport/flatprefix.go +++ /dev/null @@ -1,169 +0,0 @@ -package gatewaytransport - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "slices" - "strings" - - "github.com/dewebprotocol/malt-core/derivation" - "github.com/dewebprotocol/malt-core/maltcid" - cid "github.com/ipfs/go-cid" -) - -const FlatPrefixProfile = "gateway.evaluation-flat-prefix-mutation/1" - -type FlatPrefixMutation struct { - OperationID string - BaseRoot cid.Cid - Initial bool - Changes []FlatPrefixChange -} - -type FlatPrefixChange struct { - Label []byte - Before cid.Cid - After cid.Cid -} - -type FlatPrefixResult struct { - Root cid.Cid - ValidationAndStageNanos uint64 - PersistNanos uint64 - WriteAccounting WriteAccounting -} - -func (c *Client) ApplyEvaluationFlatPrefix(ctx context.Context, authorizationToken string, value FlatPrefixMutation) (FlatPrefixResult, error) { - if !canonicalLowerSHA256(authorizationToken) { - return FlatPrefixResult{}, fmt.Errorf("evaluation flat-prefix authorization token must be a canonical SHA-256") - } - if !validFlatPrefixOperationID(value.OperationID) || len(value.Changes) == 0 || len(value.Changes) > 65_536 { - return FlatPrefixResult{}, fmt.Errorf("evaluation flat-prefix mutation is incomplete") - } - if value.Initial == value.BaseRoot.Defined() { - return FlatPrefixResult{}, fmt.Errorf("evaluation flat-prefix base/initial relationship is invalid") - } - if value.BaseRoot.Defined() && (!currentFlatPrefixRoot(value.BaseRoot)) { - return FlatPrefixResult{}, fmt.Errorf("evaluation flat-prefix base is not a KZG Prefix/AA=1 Root") - } - type wireChange struct { - Label []byte `json:"label"` - Before string `json:"before,omitempty"` - After string `json:"after,omitempty"` - } - body := struct { - Profile string `json:"profile"` - OperationID string `json:"operation_id"` - BaseRoot string `json:"base_root,omitempty"` - Initial bool `json:"initial"` - Changes []wireChange `json:"changes"` - }{ - Profile: FlatPrefixProfile, OperationID: value.OperationID, Initial: value.Initial, - Changes: make([]wireChange, len(value.Changes)), - } - if value.BaseRoot.Defined() { - body.BaseRoot = value.BaseRoot.String() - } - paths := make([]string, len(value.Changes)) - for index, change := range value.Changes { - if len(change.Label) == 0 || (!change.Before.Defined() && !change.After.Defined()) || - (change.Before.Defined() && change.After.Defined() && change.Before.Equals(change.After)) || - value.Initial && (change.Before.Defined() || !change.After.Defined()) { - return FlatPrefixResult{}, fmt.Errorf("evaluation flat-prefix change %d is invalid", index) - } - paths[index] = string(change.Label) - body.Changes[index].Label = change.Label - if change.Before.Defined() { - body.Changes[index].Before = change.Before.String() - } - if change.After.Defined() { - body.Changes[index].After = change.After.String() - } - } - if !slices.IsSorted(paths) { - return FlatPrefixResult{}, fmt.Errorf("evaluation flat-prefix changes are not sorted") - } - for index := 1; index < len(paths); index++ { - if paths[index] == paths[index-1] { - return FlatPrefixResult{}, fmt.Errorf("evaluation flat-prefix changes repeat a path") - } - } - encoded, err := json.Marshal(body) - if err != nil { - return FlatPrefixResult{}, err - } - request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint("/v1/evaluation/rq3/flat-prefix"), bytes.NewReader(encoded)) - if err != nil { - return FlatPrefixResult{}, err - } - request.Header.Set("Content-Type", "application/json") - request.Header.Set(BootstrapAuthorizationTokenHeader, authorizationToken) - response, err := c.plainHTTP.Do(request) - if err != nil { - return FlatPrefixResult{}, err - } - defer response.Body.Close() - if response.StatusCode < 200 || response.StatusCode >= 300 { - return FlatPrefixResult{}, c.responseError(response) - } - if err := requireJSONNoStore(response); err != nil { - return FlatPrefixResult{}, err - } - raw, err := readBounded(response.Body, c.maxJSONResponseBytes, "Gateway evaluation flat-prefix response") - if err != nil { - return FlatPrefixResult{}, err - } - if err := rejectDuplicateJSONKeys(raw); err != nil { - return FlatPrefixResult{}, fmt.Errorf("Gateway evaluation flat-prefix response: %w", err) - } - var wire struct { - Profile string `json:"profile"` - Root string `json:"root"` - ValidationAndStageNanos uint64 `json:"validation_and_stage_nanos"` - PersistNanos uint64 `json:"persist_nanos"` - WriteAccounting WriteAccounting `json:"write_accounting"` - } - if err := decodeStrict(raw, &wire); err != nil { - return FlatPrefixResult{}, fmt.Errorf("decode Gateway evaluation flat-prefix response: %w", err) - } - root, err := cid.Parse(wire.Root) - if err != nil || wire.Profile != FlatPrefixProfile || !root.Defined() || - !currentFlatPrefixRoot(root) { - return FlatPrefixResult{}, fmt.Errorf("Gateway evaluation flat-prefix returned an invalid root") - } - if err := validateWriteAccounting(wire.WriteAccounting); err != nil || !wire.WriteAccounting.Available || - strings.TrimSpace(wire.WriteAccounting.UnavailableReason) != "" { - return FlatPrefixResult{}, fmt.Errorf("Gateway evaluation flat-prefix returned invalid exact write accounting") - } - if len(wire.WriteAccounting.Categories) != 3 || - wire.WriteAccounting.Categories[1] != (WriteCategoryAccounting{Category: "arctable-lineage-metadata"}) || - wire.WriteAccounting.Categories[2] != (WriteCategoryAccounting{Category: "root-version-metadata"}) { - return FlatPrefixResult{}, fmt.Errorf("Gateway evaluation flat-prefix persisted non-ArcSet metadata") - } - return FlatPrefixResult{ - Root: root, ValidationAndStageNanos: wire.ValidationAndStageNanos, PersistNanos: wire.PersistNanos, - WriteAccounting: wire.WriteAccounting, - }, nil -} - -func validFlatPrefixOperationID(value string) bool { - if value == "" || len(value) > 128 { - return false - } - for index, character := range value { - if character >= 0x61 && character <= 0x7a || character >= 0x30 && character <= 0x39 || - index > 0 && (character == 0x2e || character == 0x5f || character == 0x2d) { - continue - } - return false - } - return true -} - -func currentFlatPrefixRoot(root cid.Cid) bool { - d, _, err := maltcid.ParseRoot(root) - return err == nil && d.Layout == maltcid.Prefix && d.DerivationProfile == uint8(derivation.SHA256) && d.Profile == maltcid.KZG4096 -} diff --git a/internal/evaluation/machine/machine-descriptor.schema.json b/internal/evaluation/machine/machine-descriptor.schema.json deleted file mode 100644 index ffa3698..0000000 --- a/internal/evaluation/machine/machine-descriptor.schema.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://dewebprotocol.github.io/malt/schemas/malt-evaluation-machine-descriptor-v1.json", - "title": "MALT paper evaluation machine descriptor", - "type": "object", - "additionalProperties": false, - "required": ["schema_version", "id", "identity", "classification", "classification_evidence_source"], - "properties": { - "schema_version": {"const": "malt-evaluation-machine-descriptor/v1"}, - "id": {"type": "string", "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$"}, - "identity": { - "type": "object", - "additionalProperties": false, - "required": ["os", "architecture", "cpu_model", "cpu_model_source", "board_model", "board_model_source", "logical_cores", "logical_cores_source", "memory_bytes", "memory_source"], - "properties": { - "os": {"type": "string", "minLength": 1, "maxLength": 512}, - "architecture": {"type": "string", "minLength": 1, "maxLength": 512}, - "cpu_model": {"type": "string", "minLength": 1, "maxLength": 512}, - "cpu_model_source": {"type": "string", "minLength": 1, "maxLength": 512}, - "board_model": {"type": "string", "minLength": 1, "maxLength": 512}, - "board_model_source": {"type": "string", "minLength": 1, "maxLength": 512}, - "logical_cores": {"type": "integer", "minimum": 1, "maximum": 4096}, - "logical_cores_source": {"const": "go-runtime:NumCPU"}, - "memory_bytes": {"type": "integer", "minimum": 67108864}, - "memory_source": {"const": "linux:/proc/meminfo:MemTotal"} - } - }, - "classification": {"enum": ["general-purpose", "low-power-arm"]}, - "classification_evidence_source": {"type": "string", "minLength": 1, "maxLength": 1024} - } -} diff --git a/internal/evaluation/machine/machine.go b/internal/evaluation/machine/machine.go deleted file mode 100644 index ba23a21..0000000 --- a/internal/evaluation/machine/machine.go +++ /dev/null @@ -1,250 +0,0 @@ -// Package machine pins and verifies the physical machine identity used by the -// paper evaluator. A low-power classification is accepted only after the -// descriptor's actual OS, architecture, CPU, board, cores, and memory match a -// local probe. -package machine - -import ( - "bytes" - "crypto/sha256" - _ "embed" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "regexp" - "strings" -) - -const ( - SchemaVersion = "malt-evaluation-machine-descriptor/v1" - ClassGeneral = "general-purpose" - ClassLowPower = "low-power-arm" - - MaxDescriptorBytes = 64 << 10 -) - -//go:embed machine-descriptor.schema.json -var SchemaJSON []byte - -var idPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$`) - -type Identity struct { - OS string `json:"os"` - Architecture string `json:"architecture"` - CPUModel string `json:"cpu_model"` - CPUModelSource string `json:"cpu_model_source"` - BoardModel string `json:"board_model"` - BoardModelSource string `json:"board_model_source"` - LogicalCores uint32 `json:"logical_cores"` - LogicalCoresSource string `json:"logical_cores_source"` - MemoryBytes uint64 `json:"memory_bytes"` - MemorySource string `json:"memory_source"` -} - -type Descriptor struct { - SchemaVersion string `json:"schema_version"` - ID string `json:"id"` - Identity Identity `json:"identity"` - Classification string `json:"classification"` - ClassificationEvidenceSource string `json:"classification_evidence_source"` -} - -type Evidence struct { - Descriptor *Descriptor - SHA256 string - Bytes uint64 - Probe Identity -} - -func Decode(data []byte) (*Descriptor, error) { - if err := rejectDuplicateKeys(data); err != nil { - return nil, fmt.Errorf("decode machine descriptor: %w", err) - } - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.DisallowUnknownFields() - var descriptor Descriptor - if err := decoder.Decode(&descriptor); err != nil { - return nil, fmt.Errorf("decode machine descriptor: %w", err) - } - if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { - return nil, fmt.Errorf("machine descriptor must contain exactly one JSON object") - } - if err := descriptor.Validate(); err != nil { - return nil, err - } - return &descriptor, nil -} - -func rejectDuplicateKeys(data []byte) error { - decoder := json.NewDecoder(bytes.NewReader(data)) - var value func() error - value = func() error { - token, err := decoder.Token() - if err != nil { - return err - } - delimiter, ok := token.(json.Delim) - if !ok { - return nil - } - if delimiter == '[' { - for decoder.More() { - if err := value(); err != nil { - return err - } - } - _, err := decoder.Token() - return err - } - if delimiter != '{' { - return fmt.Errorf("unexpected JSON delimiter") - } - keys := make(map[string]struct{}) - for decoder.More() { - keyToken, err := decoder.Token() - if err != nil { - return err - } - key, ok := keyToken.(string) - if !ok { - return fmt.Errorf("object key is not a string") - } - if _, duplicate := keys[key]; duplicate { - return fmt.Errorf("duplicate JSON field %q", key) - } - keys[key] = struct{}{} - if err := value(); err != nil { - return err - } - } - _, err = decoder.Token() - return err - } - if err := value(); err != nil { - return err - } - if _, err := decoder.Token(); !errors.Is(err, io.EOF) { - return fmt.Errorf("JSON contains trailing data") - } - return nil -} - -func (d Descriptor) Validate() error { - if d.SchemaVersion != SchemaVersion || !idPattern.MatchString(d.ID) { - return fmt.Errorf("machine descriptor schema or ID is invalid") - } - identity := d.Identity - for name, value := range map[string]string{ - "os": identity.OS, "architecture": identity.Architecture, - "cpu_model": identity.CPUModel, "cpu_model_source": identity.CPUModelSource, - "board_model": identity.BoardModel, "board_model_source": identity.BoardModelSource, - "logical_cores_source": identity.LogicalCoresSource, "memory_source": identity.MemorySource, - } { - if value != normalize(value) || len(value) == 0 || len(value) > 512 { - return fmt.Errorf("machine descriptor %s is empty, non-canonical, or too long", name) - } - } - if identity.LogicalCores == 0 || identity.LogicalCores > 4096 || identity.MemoryBytes < 64<<20 { - return fmt.Errorf("machine descriptor cores or memory is outside evaluator bounds") - } - if d.Classification != ClassGeneral && d.Classification != ClassLowPower || normalize(d.ClassificationEvidenceSource) != d.ClassificationEvidenceSource || len(d.ClassificationEvidenceSource) == 0 || len(d.ClassificationEvidenceSource) > 1024 { - return fmt.Errorf("machine descriptor classification evidence is invalid") - } - if d.Classification == ClassLowPower && identity.Architecture != "arm" && identity.Architecture != "arm64" { - return fmt.Errorf("low-power-arm classification requires an ARM architecture") - } - return nil -} - -func (d Descriptor) LowPowerARM() bool { return d.Classification == ClassLowPower } - -func Match(descriptor *Descriptor, actual Identity) error { - if descriptor == nil { - return fmt.Errorf("machine descriptor is nil") - } - if err := descriptor.Validate(); err != nil { - return err - } - expected := descriptor.Identity - if actual != expected { - return fmt.Errorf("actual machine identity does not match descriptor %q: actual=%+v expected=%+v", descriptor.ID, actual, expected) - } - return nil -} - -// LoadPinned rejects symlinks and file replacement, verifies the registered -// bytes/digest, strictly decodes the descriptor, probes the host, and matches -// the complete physical identity before returning evidence. -func LoadPinned(path, expectedSHA256 string, expectedBytes int64) (*Evidence, error) { - if path == "" || !canonicalSHA256(expectedSHA256) || expectedBytes <= 0 || expectedBytes > MaxDescriptorBytes { - return nil, fmt.Errorf("machine descriptor artifact pin is incomplete") - } - lstat, err := os.Lstat(path) - if err != nil || lstat.Mode()&os.ModeSymlink != 0 || !lstat.Mode().IsRegular() || lstat.Size() != expectedBytes { - return nil, fmt.Errorf("machine descriptor is not the pinned regular file: %v", err) - } - file, err := os.Open(path) - if err != nil { - return nil, err - } - opened, err := file.Stat() - if err != nil || !opened.Mode().IsRegular() || !os.SameFile(lstat, opened) { - _ = file.Close() - return nil, fmt.Errorf("machine descriptor changed before it was opened") - } - data, readErr := io.ReadAll(io.LimitReader(file, MaxDescriptorBytes+1)) - closeErr := file.Close() - if readErr != nil { - return nil, readErr - } - if closeErr != nil { - return nil, closeErr - } - post, err := os.Lstat(path) - if err != nil || !os.SameFile(lstat, post) || post.Mode()&os.ModeSymlink != 0 || int64(len(data)) != expectedBytes { - return nil, fmt.Errorf("machine descriptor changed while it was read") - } - digest := sha256.Sum256(data) - encodedDigest := hex.EncodeToString(digest[:]) - if encodedDigest != expectedSHA256 { - return nil, fmt.Errorf("machine descriptor SHA-256 does not match its pin") - } - descriptor, err := Decode(data) - if err != nil { - return nil, err - } - actual, err := Probe() - if err != nil { - return nil, err - } - if err := Match(descriptor, actual); err != nil { - return nil, err - } - return &Evidence{Descriptor: descriptor, SHA256: encodedDigest, Bytes: uint64(len(data)), Probe: actual}, nil -} - -func NewDescriptor(id, classification, evidenceSource string, identity Identity) (*Descriptor, error) { - descriptor := &Descriptor{ - SchemaVersion: SchemaVersion, ID: id, Identity: identity, - Classification: classification, ClassificationEvidenceSource: normalize(evidenceSource), - } - if err := descriptor.Validate(); err != nil { - return nil, err - } - return descriptor, nil -} - -func normalize(value string) string { - return strings.Join(strings.Fields(strings.TrimSpace(strings.Trim(value, "\x00"))), " ") -} - -func canonicalSHA256(value string) bool { - if len(value) != sha256.Size*2 || strings.ToLower(value) != value { - return false - } - _, err := hex.DecodeString(value) - return err == nil -} diff --git a/internal/evaluation/machine/machine_test.go b/internal/evaluation/machine/machine_test.go deleted file mode 100644 index 7f9ca55..0000000 --- a/internal/evaluation/machine/machine_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package machine - -import ( - "crypto/sha256" - "encoding/hex" - "encoding/json" - "os" - "path/filepath" - "testing" -) - -func TestPinnedDescriptorMatchesActualMachine(t *testing.T) { - identity, err := Probe() - if err != nil { - t.Fatal(err) - } - descriptor, err := NewDescriptor("host-under-test", ClassGeneral, "test-suite:registered-platform-evidence", identity) - if err != nil { - t.Fatal(err) - } - raw, err := json.Marshal(descriptor) - if err != nil { - t.Fatal(err) - } - path := filepath.Join(t.TempDir(), "machine.json") - if err := os.WriteFile(path, raw, 0o600); err != nil { - t.Fatal(err) - } - digest := sha256.Sum256(raw) - evidence, err := LoadPinned(path, hex.EncodeToString(digest[:]), int64(len(raw))) - if err != nil { - t.Fatal(err) - } - if evidence.Descriptor.ID != descriptor.ID || evidence.Probe != identity { - t.Fatalf("machine evidence = %#v", evidence) - } - - hostile := *descriptor - hostile.Identity.CPUModel += " hostile" - if err := Match(&hostile, identity); err == nil { - t.Fatal("mismatched CPU model was accepted") - } -} - -func TestDescriptorDecodeIsStrict(t *testing.T) { - if _, err := Decode([]byte(`{"schema_version":"malt-evaluation-machine-descriptor/v1","schema_version":"malt-evaluation-machine-descriptor/v1"}`)); err == nil { - t.Fatal("duplicate descriptor field was accepted") - } - identity, err := Probe() - if err != nil { - t.Fatal(err) - } - descriptor, err := NewDescriptor("host", ClassGeneral, "test:evidence", identity) - if err != nil { - t.Fatal(err) - } - raw, _ := json.Marshal(descriptor) - raw = append(append([]byte(nil), raw[:len(raw)-1]...), []byte(`,"unknown":true}`)...) - if _, err := Decode(raw); err == nil { - t.Fatal("unknown descriptor field was accepted") - } -} diff --git a/internal/evaluation/machine/probe_linux.go b/internal/evaluation/machine/probe_linux.go deleted file mode 100644 index 5251785..0000000 --- a/internal/evaluation/machine/probe_linux.go +++ /dev/null @@ -1,102 +0,0 @@ -//go:build linux - -package machine - -import ( - "bufio" - "fmt" - "os" - "runtime" - "strconv" - "strings" -) - -func Probe() (Identity, error) { - cpuModel, cpuSource, err := probeCPUModel() - if err != nil { - return Identity{}, err - } - boardModel, boardSource, err := probeBoardModel() - if err != nil { - return Identity{}, err - } - memory, err := probeMemory() - if err != nil { - return Identity{}, err - } - cores := runtime.NumCPU() - if cores <= 0 || cores > 4096 { - return Identity{}, fmt.Errorf("runtime logical core count is outside evaluator bounds") - } - return Identity{ - OS: runtime.GOOS, Architecture: runtime.GOARCH, - CPUModel: cpuModel, CPUModelSource: cpuSource, - BoardModel: boardModel, BoardModelSource: boardSource, - LogicalCores: uint32(cores), LogicalCoresSource: "go-runtime:NumCPU", - MemoryBytes: memory, MemorySource: "linux:/proc/meminfo:MemTotal", - }, nil -} - -func probeCPUModel() (string, string, error) { - data, err := os.ReadFile("/proc/cpuinfo") - if err != nil { - return "", "", fmt.Errorf("read CPU identity: %w", err) - } - values := make(map[string]string) - scanner := bufio.NewScanner(strings.NewReader(string(data))) - for scanner.Scan() { - key, value, found := strings.Cut(scanner.Text(), ":") - if !found { - continue - } - key, value = strings.ToLower(strings.TrimSpace(key)), normalize(value) - if value != "" && values[key] == "" { - values[key] = value - } - } - if err := scanner.Err(); err != nil { - return "", "", err - } - for _, key := range []string{"model name", "hardware", "model"} { - if values[key] != "" { - return values[key], "linux:/proc/cpuinfo:" + key, nil - } - } - return "", "", fmt.Errorf("/proc/cpuinfo has no bounded CPU model field") -} - -func probeBoardModel() (string, string, error) { - for _, path := range []string{"/proc/device-tree/model", "/sys/firmware/devicetree/base/model", "/sys/class/dmi/id/product_name"} { - data, err := os.ReadFile(path) - if err != nil { - continue - } - if value := normalize(string(data)); value != "" { - return value, "linux:" + path, nil - } - } - return "", "", fmt.Errorf("no device-tree or DMI board model is available") -} - -func probeMemory() (uint64, error) { - file, err := os.Open("/proc/meminfo") - if err != nil { - return 0, err - } - defer file.Close() - scanner := bufio.NewScanner(file) - for scanner.Scan() { - fields := strings.Fields(scanner.Text()) - if len(fields) == 3 && fields[0] == "MemTotal:" && fields[2] == "kB" { - value, err := strconv.ParseUint(fields[1], 10, 64) - if err != nil || value > ^uint64(0)/1024 { - return 0, fmt.Errorf("parse /proc/meminfo MemTotal") - } - return value * 1024, nil - } - } - if err := scanner.Err(); err != nil { - return 0, err - } - return 0, fmt.Errorf("/proc/meminfo omits MemTotal") -} diff --git a/internal/evaluation/machine/probe_other.go b/internal/evaluation/machine/probe_other.go deleted file mode 100644 index ec0c784..0000000 --- a/internal/evaluation/machine/probe_other.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !linux - -package machine - -import "fmt" - -func Probe() (Identity, error) { - return Identity{}, fmt.Errorf("paper machine identity probing is unavailable on this operating system") -} diff --git a/internal/evaluation/rq2e0/file.go b/internal/evaluation/rq2e0/file.go deleted file mode 100644 index be4bf3e..0000000 --- a/internal/evaluation/rq2e0/file.go +++ /dev/null @@ -1,70 +0,0 @@ -package rq2e0 - -import ( - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "regexp" -) - -var fileIDPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,127}$`) - -// FilePin is a live descriptor for one formal-E0 regular-file input. -type FilePin struct { - FileID string `json:"e0_file_id"` - Path string `json:"path"` - SHA256 string `json:"sha256"` - Bytes int64 `json:"bytes"` -} - -// DescribeRegularFile rejects symlinks and replacement races while deriving -// the exact digest and size consumed by a production worker. -func DescribeRegularFile(fileID, path string) (FilePin, error) { - if !fileIDPattern.MatchString(fileID) || !filepath.IsAbs(path) || filepath.Clean(path) != path { - return FilePin{}, errors.New("invalid RQ2 E0 regular-file identity or path") - } - info, err := os.Lstat(path) - if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() || info.Size() <= 0 { - return FilePin{}, fmt.Errorf("RQ2 E0 input is not a nonempty regular non-symlink file: %v", err) - } - file, err := os.Open(path) - if err != nil { - return FilePin{}, err - } - opened, err := file.Stat() - if err != nil || !opened.Mode().IsRegular() || !os.SameFile(info, opened) { - _ = file.Close() - return FilePin{}, errors.New("RQ2 E0 input changed before it was opened") - } - hash := sha256.New() - written, copyErr := io.Copy(hash, file) - closeErr := file.Close() - if copyErr != nil { - return FilePin{}, copyErr - } - if closeErr != nil { - return FilePin{}, closeErr - } - post, err := os.Lstat(path) - if err != nil || post.Mode()&os.ModeSymlink != 0 || !os.SameFile(info, post) || written != info.Size() { - return FilePin{}, errors.New("RQ2 E0 input changed while it was read") - } - return FilePin{FileID: fileID, Path: path, SHA256: hex.EncodeToString(hash.Sum(nil)), Bytes: written}, nil -} - -// ValidateFilePin verifies that a declared pin still identifies the same -// regular file at the point where it is consumed. -func ValidateFilePin(pin FilePin) error { - live, err := DescribeRegularFile(pin.FileID, pin.Path) - if err != nil { - return err - } - if live.SHA256 != pin.SHA256 || live.Bytes != pin.Bytes { - return errors.New("RQ2 E0 input does not match its SHA-256/size pin") - } - return nil -} diff --git a/internal/evaluation/rq2e0/fixture.go b/internal/evaluation/rq2e0/fixture.go deleted file mode 100644 index d0813a9..0000000 --- a/internal/evaluation/rq2e0/fixture.go +++ /dev/null @@ -1,70 +0,0 @@ -package rq2e0 - -import ( - "context" - "fmt" - - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture" - "github.com/dewebprotocol/malt-core/auth/commitment/ipa" - "github.com/dewebprotocol/malt-core/auth/commitment/kzg" - "github.com/dewebprotocol/malt-core/engine" - cid "github.com/ipfs/go-cid" -) - -// BuildFixture computes exact typed KZG and IPA roots from declared source -// bytes, then independently checks the full source oracle for each graph. -func BuildFixture(ctx context.Context, source *rq2fixture.SourceDefinition) (*rq2fixture.Fixture, error) { - if ctx == nil || source == nil { - return nil, fmt.Errorf("RQ2 E0 fixture source or context is nil") - } - roots := make([]rq2fixture.RootBinding, 0, 2) - for _, backend := range []string{"kzg", "ipa"} { - e, err := fixtureEngine(backend) - if err != nil { - return nil, err - } - candidates, err := source.Candidates(ctx, e, backend) - if err != nil { - return nil, err - } - root, err := cid.Parse(candidates[len(candidates)-1].Root) - if err != nil { - return nil, err - } - roots = append(roots, rq2fixture.RootBinding{Backend: backend, CID: root.String()}) - } - fixture, err := source.Fixture(roots) - if err != nil { - return nil, err - } - for _, backend := range []string{"kzg", "ipa"} { - e, err := fixtureEngine(backend) - if err != nil { - return nil, err - } - if _, err := fixture.Candidates(ctx, e, backend); err != nil { - return nil, err - } - } - return fixture, nil -} -func fixtureEngine(backend string) (*engine.Engine, error) { - var scheme engine.Profile - var err error - switch backend { - case "kzg": - scheme, err = kzg.NewScheme() - case "ipa": - scheme, err = ipa.NewScheme() - default: - return nil, fmt.Errorf("unsupported backend %q", backend) - } - if err != nil { - return nil, err - } - profiles := engine.NewRegistry() - if err := profiles.Register(scheme); err != nil { - return nil, err - } - return engine.New(profiles), nil -} diff --git a/internal/evaluation/rq2e0/gateway.go b/internal/evaluation/rq2e0/gateway.go deleted file mode 100644 index c29d74e..0000000 --- a/internal/evaluation/rq2e0/gateway.go +++ /dev/null @@ -1,230 +0,0 @@ -package rq2e0 - -import ( - "context" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/http/httptest" - "strconv" - "strings" - "sync" - "time" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture" - clienttransport "github.com/dewebprotocol/malt-client/transport" - "github.com/dewebprotocol/malt-core/engine" - "github.com/dewebprotocol/malt-core/maltcid" - "github.com/dewebprotocol/malt-core/protocol" - "github.com/dewebprotocol/malt-core/sdk/authentication" - cid "github.com/ipfs/go-cid" -) - -// ConformanceGateway is an in-memory HTTP oracle for writer correctness only. -// Actual durability, byte accounting and snapshot E0 use the separately tested -// Gateway executable. This oracle emits unavailable write accounting and may -// never be used as a paper performance or persistence measurement source. -type ConformanceGateway struct { - mu sync.Mutex - token string - candidates map[string]protocol.AuthenticationCandidate - payloads map[string]bool - receipts map[string]protocol.AuthenticationReceipt - engine *engine.Engine - server *httptest.Server - operations uint64 -} - -func NewConformanceGateway(fixture *rq2fixture.Fixture, backend, token string) (*ConformanceGateway, cid.Cid, error) { - if fixture == nil || len(token) != 64 { - return nil, cid.Undef, errors.New("invalid RQ2 writer conformance input") - } - e, err := fixtureEngine(backend) - if err != nil { - return nil, cid.Undef, err - } - candidates, err := fixture.Candidates(context.Background(), e, backend) - if err != nil { - return nil, cid.Undef, err - } - g := &ConformanceGateway{token: token, engine: e, candidates: map[string]protocol.AuthenticationCandidate{}, payloads: map[string]bool{}, receipts: map[string]protocol.AuthenticationReceipt{}} - for _, c := range candidates { - g.candidates[c.Root] = c - } - for _, f := range fixture.DirectFiles { - g.payloads[f.CID] = true - } - for _, f := range fixture.ListFiles { - for _, chunk := range f.Chunks { - g.payloads[chunk.CID] = true - } - } - g.server = httptest.NewServer(g) - root, _ := cid.Parse(candidates[len(candidates)-1].Root) - return g, root, nil -} -func (g *ConformanceGateway) URL() string { return g.server.URL } - -func (g *ConformanceGateway) Close() { - if g != nil && g.server != nil { - g.server.Close() - g.server = nil - } -} - -func (g *ConformanceGateway) Operations() uint64 { - g.mu.Lock() - defer g.mu.Unlock() - return g.operations -} - -func (g *ConformanceGateway) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Cache-Control", "private, no-store") - if r.Header.Get(gatewaytransport.InstanceTokenHeader) != g.token { - http.Error(w, "incorrect instance token", http.StatusUnauthorized) - return - } - switch { - case r.URL.Path == "/healthz" && r.Method == http.MethodGet: - _ = json.NewEncoder(w).Encode(map[string]any{"status": "ok", "evaluation_instance_token": g.token}) - case strings.HasPrefix(r.URL.Path, "/v1/evaluation/authentication/candidates/") && r.Method == http.MethodGet: - root := strings.TrimPrefix(r.URL.Path, "/v1/evaluation/authentication/candidates/") - g.mu.Lock() - candidate, ok := g.candidates[root] - g.mu.Unlock() - if !ok { - http.Error(w, "unknown Root", http.StatusNotFound) - return - } - _ = json.NewEncoder(w).Encode(candidate) - case r.URL.Path == "/v1/cas/batch" && r.Method == http.MethodPost: - g.handleCASBatch(w, r) - case r.URL.Path == "/v1/evaluation/authentication/batches" && r.Method == http.MethodPost: - g.handleBatch(w, r) - default: - http.NotFound(w, r) - } -} -func (g *ConformanceGateway) handleCASBatch(w http.ResponseWriter, r *http.Request) { - var submitted struct { - Profile string `json:"profile"` - Blocks []struct { - Codec uint64 `json:"codec"` - Data []byte `json:"data"` - } `json:"blocks"` - } - decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, protocol.MaxVerificationJSONBytes)) - decoder.DisallowUnknownFields() - if err := decoder.Decode(&submitted); err != nil || submitted.Profile != clienttransport.CASPutBatchProfile || len(submitted.Blocks) == 0 || len(submitted.Blocks) > 64 { - http.Error(w, "invalid CAS batch", http.StatusBadRequest) - return - } - if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { - http.Error(w, "trailing JSON", http.StatusBadRequest) - return - } - keys := make([]cid.Cid, len(submitted.Blocks)) - results := make([]map[string]string, len(keys)) - for i, b := range submitted.Blocks { - key, err := clientcas.CIDForBlock(clientcas.Block{Codec: b.Codec, Data: b.Data}) - if err != nil || b.Codec != cid.Raw { - http.Error(w, "invalid raw payload", http.StatusBadRequest) - return - } - keys[i] = key - results[i] = map[string]string{"cid": key.String(), "status": "stored"} - } - g.mu.Lock() - for _, key := range keys { - g.payloads[key.String()] = true - } - g.mu.Unlock() - w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(map[string]any{"profile": clienttransport.CASPutBatchProfile, "results": results}) -} -func (g *ConformanceGateway) handleBatch(w http.ResponseWriter, r *http.Request) { - started := time.Now() - raw, err := io.ReadAll(http.MaxBytesReader(w, r.Body, protocol.MaxVerificationJSONBytes)) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - batch, err := protocol.DecodeAuthenticationBatch(raw) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - digest, err := batch.Digest() - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - g.mu.Lock() - defer g.mu.Unlock() - if receipt, exists := g.receipts[batch.TransactionID]; exists { - if receipt.Digest != digest { - http.Error(w, "transaction substitution", http.StatusConflict) - return - } - g.writeReceipt(w, receipt, true, uint64(time.Since(started)), 0, 0) - return - } - if _, ok := g.candidates[batch.Base]; !ok { - http.Error(w, "unknown batch base", http.StatusConflict) - return - } - if err := authentication.ValidateBatch(r.Context(), g.engine, batch); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - staged := map[string]bool{} - for _, candidate := range batch.Candidates { - if candidate.Previous != "" { - if _, ok := g.candidates[candidate.Previous]; !ok && !staged[candidate.Previous] { - http.Error(w, "missing previous Root", http.StatusBadRequest) - return - } - } - for _, entry := range candidate.State.Entries { - key := entry.Target.String() - if _, _, err := maltcid.ParseRoot(entry.Target); err == nil { - if _, ok := g.candidates[key]; !ok && !staged[key] { - http.Error(w, "missing child Root", http.StatusBadRequest) - return - } - } else if !g.payloads[key] { - http.Error(w, "missing payload", http.StatusBadRequest) - return - } - } - staged[candidate.Root] = true - } - validation := uint64(time.Since(started)) - started = time.Now() - for _, candidate := range batch.Candidates { - g.candidates[candidate.Root] = candidate - } - g.operations++ - persist := uint64(time.Since(started)) - started = time.Now() - receipt := protocol.AuthenticationReceipt{Profile: protocol.AuthenticationReceiptProfile, TransactionID: batch.TransactionID, Base: batch.Base, Root: batch.Root, Digest: digest, DurableBoundary: gatewaytransport.AuthenticationDurableBoundary} - g.receipts[batch.TransactionID] = receipt - g.writeReceipt(w, receipt, false, validation, persist, uint64(time.Since(started))) -} -func (g *ConformanceGateway) writeReceipt(w http.ResponseWriter, receipt protocol.AuthenticationReceipt, retry bool, validation, persist, receiptNS uint64) { - prefix := "X-Malt-Authentication-" - for name, value := range map[string]string{"Phase-Profile": gatewaytransport.AuthenticationPhaseProfile, "Publication": "separate", "Trust": "client-owned", "Durable-Boundary": receipt.DurableBoundary, "Idempotent": strconv.FormatBool(retry), "Validation-And-Stage-Nanos": strconv.FormatUint(validation, 10), "Persist-Nanos": strconv.FormatUint(persist, 10), "Receipt-Nanos": strconv.FormatUint(receiptNS, 10)} { - w.Header().Set(prefix+name, value) - } - w.Header().Set("Server-Timing", fmt.Sprintf("authentication-validate-stage;dur=%.6f, persist;dur=%.6f, receipt;dur=%.6f", float64(validation)/1e6, float64(persist)/1e6, float64(receiptNS)/1e6)) - accounting, _ := json.Marshal(gatewaytransport.WriteAccounting{Profile: gatewaytransport.WriteAccountingProfile, Available: false, UnavailableReason: "writer-conformance-e0-no-durable-accounting", ByteMethod: gatewaytransport.WriteByteMethod, Categories: []gatewaytransport.WriteCategoryAccounting{}}) - w.Header().Set(prefix+"Write-Accounting", base64.RawURLEncoding.EncodeToString(accounting)) - w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(receipt) -} diff --git a/internal/evaluation/rq2e0/harness.go b/internal/evaluation/rq2e0/harness.go deleted file mode 100644 index e7cfa56..0000000 --- a/internal/evaluation/rq2e0/harness.go +++ /dev/null @@ -1,163 +0,0 @@ -package rq2e0 - -import ( - "bufio" - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "sync" - "time" -) - -const maximumWorkerLine = 1 << 20 - -// Harness runs the production worker function over its real JSONL process -// boundary while allowing a formal self-test to advance the accepted root one -// response at a time. -type Harness struct { - input *io.PipeWriter - lines <-chan harnessLine - done <-chan error - timeout time.Duration - once sync.Once -} - -type harnessLine struct { - data []byte - err error -} - -// StartHarness starts one production worker callback. The callback must use -// input/output exactly as its normal stdin/stdout path does. -func StartHarness(timeout time.Duration, run func(io.Reader, io.Writer) error) (*Harness, error) { - if timeout <= 0 || timeout > 10*time.Minute || run == nil { - return nil, errors.New("invalid RQ2 E0 worker harness") - } - inputReader, inputWriter := io.Pipe() - outputReader, outputWriter := io.Pipe() - done := make(chan error, 1) - go func() { - err := run(inputReader, outputWriter) - _ = outputWriter.CloseWithError(err) - _ = inputReader.CloseWithError(err) - done <- err - }() - lines := make(chan harnessLine, 1) - go func() { - defer close(lines) - scanner := bufio.NewScanner(outputReader) - scanner.Buffer(make([]byte, 64<<10), maximumWorkerLine) - for scanner.Scan() { - lines <- harnessLine{data: append([]byte(nil), scanner.Bytes()...)} - } - if err := scanner.Err(); err != nil { - lines <- harnessLine{err: err} - } - _ = outputReader.Close() - }() - return &Harness{input: inputWriter, lines: lines, done: done, timeout: timeout}, nil -} - -// Exchange sends one strict JSON object and decodes one strict JSON response. -func (h *Harness) Exchange(request, response any) error { - encoded, err := json.Marshal(request) - if err != nil { - return err - } - if _, err := h.input.Write(append(encoded, '\n')); err != nil { - return err - } - select { - case line, ok := <-h.lines: - if !ok { - return errors.New("RQ2 E0 worker closed stdout before its response") - } - if line.err != nil { - return line.err - } - decoder := json.NewDecoder(bytes.NewReader(line.data)) - decoder.DisallowUnknownFields() - if err := decoder.Decode(response); err != nil { - return err - } - if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { - return errors.New("RQ2 E0 worker response contains trailing JSON") - } - return nil - case <-time.After(h.timeout): - return errors.New("RQ2 E0 worker response timeout") - } -} - -// WriteRaw writes one raw JSONL request for malformed-input hostile cases. -func (h *Harness) WriteRaw(value []byte) error { - if len(value) == 0 || bytes.IndexByte(value, '\n') >= 0 { - return errors.New("invalid raw RQ2 E0 request") - } - _, err := h.input.Write(append(append([]byte(nil), value...), '\n')) - return err -} - -// WaitError closes stdin and requires the worker to fail. It is used only for -// hostile cases that are rejected before a valid response can be emitted. -func (h *Harness) WaitError() error { - _ = h.input.Close() - select { - case err := <-h.done: - h.once.Do(func() {}) - if err == nil { - return errors.New("RQ2 E0 hostile worker unexpectedly succeeded") - } - return nil - case <-time.After(h.timeout): - h.Abort() - return errors.New("RQ2 E0 hostile worker did not terminate") - } -} - -// Close completes a successful worker after its session-end response and -// rejects any trailing output. -func (h *Harness) Close() error { - if h == nil || h.input == nil { - return errors.New("RQ2 E0 worker harness is closed") - } - _ = h.input.Close() - select { - case err := <-h.done: - h.input = nil - h.once.Do(func() {}) - if err != nil { - return err - } - for line := range h.lines { - if line.err != nil { - return line.err - } - return fmt.Errorf("RQ2 E0 worker emitted trailing output %q", line.data) - } - return nil - case <-time.After(h.timeout): - h.Abort() - return errors.New("RQ2 E0 worker did not exit") - } -} - -// Abort stops an incomplete/hostile harness without claiming a successful -// production session. -func (h *Harness) Abort() { - if h == nil { - return - } - h.once.Do(func() { - if h.input != nil { - _ = h.input.CloseWithError(errors.New("formal E0 hostile abort")) - } - select { - case <-h.done: - case <-time.After(5 * time.Second): - } - h.input = nil - }) -} diff --git a/internal/evaluation/rq2fixture/authentication.go b/internal/evaluation/rq2fixture/authentication.go deleted file mode 100644 index 0db5cf2..0000000 --- a/internal/evaluation/rq2fixture/authentication.go +++ /dev/null @@ -1,100 +0,0 @@ -package rq2fixture - -import ( - "context" - "fmt" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - "github.com/dewebprotocol/malt-client/internal/evaluation/authenticationgraph" - unixfsmodel "github.com/dewebprotocol/malt-client/unixfs/model" - "github.com/dewebprotocol/malt-core/auth/coordinate" - "github.com/dewebprotocol/malt-core/derivation" - "github.com/dewebprotocol/malt-core/engine" - "github.com/dewebprotocol/malt-core/maltcid" - "github.com/dewebprotocol/malt-core/protocol" - "github.com/dewebprotocol/malt-core/sdk/authentication" - cid "github.com/ipfs/go-cid" -) - -// Candidates builds measured Positional file objects before their Prefix AA1 -// directory root. Source paths are explicit opaque labels at the Core boundary. -func (s *SourceDefinition) Candidates(ctx context.Context, e *engine.Engine, backend string) ([]protocol.AuthenticationCandidate, error) { - if err := s.Validate(); err != nil { - return nil, err - } - profile, err := BackendProfile(backend) - if err != nil { - return nil, err - } - candidates := make([]protocol.AuthenticationCandidate, 0, len(s.ListFiles)+1) - seen := map[string]bool{} - bindings := make([]engine.Entry, 0, len(s.DirectFiles)+len(s.ListFiles)) - for _, file := range s.DirectFiles { - target, err := clientcas.CIDForBlock(clientcas.Block{Codec: cid.Raw, Data: file.Bytes}) - if err != nil { - return nil, err - } - bindings = append(bindings, engine.Entry{Label: []byte(file.Path), Target: target}) - } - for _, file := range s.ListFiles { - state := engine.State{Descriptor: maltcid.RootDescriptor{DerivationProfile: uint8(derivation.Direct), Layout: maltcid.Positional, Profile: profile}, Entries: make([]engine.Entry, len(file.Chunks))} - state.PayloadCID, err = unixfsmodel.InlineChunkMetadata(file.TotalSize, file.ChunkSize, uint64(len(file.Chunks))) - if err != nil { - return nil, err - } - for i, chunk := range file.Chunks { - target, err := clientcas.CIDForBlock(clientcas.Block{Codec: cid.Raw, Data: chunk.Bytes}) - if err != nil { - return nil, err - } - state.Entries[i] = engine.Entry{Label: coordinate.EncodeIndex(chunk.Index), Target: target} - } - candidate, err := authentication.Prepare(ctx, e, state) - if err != nil { - return nil, err - } - if !seen[candidate.Root] { - candidates = append(candidates, candidate) - seen[candidate.Root] = true - } - target, _ := cid.Parse(candidate.Root) - bindings = append(bindings, engine.Entry{Label: []byte(file.Path), Target: target}) - } - candidate, err := authentication.Prepare(ctx, e, engine.State{Descriptor: maltcid.RootDescriptor{Layout: maltcid.Prefix, DerivationProfile: uint8(derivation.SHA256), Profile: profile}, Entries: bindings}) - if err != nil { - return nil, err - } - return append(candidates, candidate), nil -} - -// Candidates independently reconstructs the fixture's exact pinned Root and -// checks the complete source oracle before returning bootstrap candidates. -func (f *Fixture) Candidates(ctx context.Context, e *engine.Engine, backend string) ([]protocol.AuthenticationCandidate, error) { - if err := f.Validate(); err != nil { - return nil, err - } - source := SourceDefinition{SchemaVersion: SourceSchemaVersion, FixtureID: f.FixtureID, MutationSeedSHA256: f.MutationSeedSHA256, Operations: f.Operations} - for _, file := range f.DirectFiles { - source.DirectFiles = append(source.DirectFiles, SourceDirectFile{Path: file.Path, Coordinate: file.Coordinate, Bytes: file.Bytes}) - } - for _, file := range f.ListFiles { - out := SourceListFile{Path: file.Path, Coordinate: file.Coordinate, ChunkSize: file.ChunkSize, TotalSize: file.TotalSize} - for _, chunk := range file.Chunks { - out.Chunks = append(out.Chunks, SourceListChunk{Index: chunk.Index, Bytes: chunk.Bytes}) - } - source.ListFiles = append(source.ListFiles, out) - } - candidates, err := source.Candidates(ctx, e, backend) - if err != nil { - return nil, err - } - root, _ := cid.Parse(candidates[len(candidates)-1].Root) - view := authenticationgraph.View{Root: root, States: make(map[string]engine.State, len(candidates))} - for _, candidate := range candidates { - view.States[candidate.Root] = candidate.State - } - if err := f.ValidateInitialGraph(view, backend); err != nil { - return nil, fmt.Errorf("reconstructed fixture: %w", err) - } - return candidates, nil -} diff --git a/internal/evaluation/rq2fixture/fixture.go b/internal/evaluation/rq2fixture/fixture.go deleted file mode 100644 index 1294abc..0000000 --- a/internal/evaluation/rq2fixture/fixture.go +++ /dev/null @@ -1,617 +0,0 @@ -// Package rq2fixture defines the bounded source fixture used by the paper RQ2 -// native and browser writers. The fixture binds the initial accepted root to -// exact source bytes and declares every operation target before measurement. -package rq2fixture - -import ( - "bytes" - "crypto/sha256" - "encoding/binary" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "regexp" - "slices" - "strings" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - "github.com/dewebprotocol/malt-client/internal/evaluation/authenticationgraph" - "github.com/dewebprotocol/malt-client/unixfs" - "github.com/dewebprotocol/malt-core/derivation" - "github.com/dewebprotocol/malt-core/maltcid" - cid "github.com/ipfs/go-cid" -) - -const ( - SchemaVersion = "malt-rq2-source-fixture/v1" - - KindDirectReplace = "direct-replace" - KindDirectInsert = "direct-insert" - KindDirectDelete = "direct-delete" - KindDirectMove = "direct-move" - KindListAppend = "list-append" - KindListReplace = "list-replace" - KindBatchInsert = "batch-insert" - KindDocumentEdit = "document-edit" - - maxFiles = 4096 - maxOperations = 256 - maxChunks = 65536 - maxDecodedBytes = 64 << 20 -) - -var identityPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$`) - -type RootBinding struct { - Backend string `json:"backend"` - CID string `json:"cid"` -} - -type DirectFile struct { - Path string `json:"path"` - Coordinate string `json:"coordinate"` - Bytes []byte `json:"bytes"` - CID string `json:"cid"` -} - -type ListChunk struct { - Index uint64 `json:"index"` - Bytes []byte `json:"bytes"` - CID string `json:"cid"` -} - -type ListFile struct { - Path string `json:"path"` - Coordinate string `json:"coordinate"` - ChunkSize uint64 `json:"chunk_size"` - TotalSize uint64 `json:"total_size"` - Chunks []ListChunk `json:"chunks"` -} - -type BatchTarget struct { - Path string `json:"path"` - Coordinate string `json:"coordinate"` - PayloadBytes uint64 `json:"payload_bytes"` -} - -// Operation fixes the source path and authenticated coordinate affected by an -// evaluator operation. Coordinates are deliberately explicit even though the -// v1 source profile requires them to equal their canonical UnixFS paths. -type Operation struct { - Name string `json:"name"` - Kind string `json:"kind"` - SourcePath string `json:"source_path,omitempty"` - SourceCoordinate string `json:"source_coordinate,omitempty"` - DestinationPath string `json:"destination_path,omitempty"` - DestinationCoordinate string `json:"destination_coordinate,omitempty"` - PayloadBytes uint64 `json:"payload_bytes,omitempty"` - ListIndex *uint64 `json:"list_index,omitempty"` - Batch []BatchTarget `json:"batch,omitempty"` -} - -type Fixture struct { - SchemaVersion string `json:"schema_version"` - FixtureID string `json:"fixture_id"` - MutationSeedSHA256 string `json:"mutation_seed_sha256"` - InitialRoots []RootBinding `json:"initial_roots"` - DirectFiles []DirectFile `json:"direct_files"` - ListFiles []ListFile `json:"list_files"` - Operations []Operation `json:"operations"` -} - -// Decode accepts exactly one JSON object, rejects unknown fields, and verifies -// all roots, CIDs, source bytes, descriptors, paths, and operation targets. -func Decode(data []byte) (*Fixture, error) { - if err := rejectDuplicateKeys(data); err != nil { - return nil, fmt.Errorf("decode RQ2 source fixture: %w", err) - } - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.DisallowUnknownFields() - var fixture Fixture - if err := decoder.Decode(&fixture); err != nil { - return nil, fmt.Errorf("decode RQ2 source fixture: %w", err) - } - if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { - return nil, fmt.Errorf("RQ2 source fixture must contain exactly one JSON object") - } - if err := fixture.Validate(); err != nil { - return nil, err - } - return &fixture, nil -} - -func rejectDuplicateKeys(data []byte) error { - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.UseNumber() - var value func() error - value = func() error { - token, err := decoder.Token() - if err != nil { - return err - } - delimiter, ok := token.(json.Delim) - if !ok { - return nil - } - switch delimiter { - case '{': - keys := make(map[string]struct{}) - for decoder.More() { - keyToken, err := decoder.Token() - if err != nil { - return err - } - key, ok := keyToken.(string) - if !ok { - return fmt.Errorf("object key is not a string") - } - if _, duplicate := keys[key]; duplicate { - return fmt.Errorf("duplicate JSON field %q", key) - } - keys[key] = struct{}{} - if err := value(); err != nil { - return err - } - } - closing, err := decoder.Token() - if err != nil || closing != json.Delim('}') { - return fmt.Errorf("unterminated JSON object") - } - case '[': - for decoder.More() { - if err := value(); err != nil { - return err - } - } - closing, err := decoder.Token() - if err != nil || closing != json.Delim(']') { - return fmt.Errorf("unterminated JSON array") - } - default: - return fmt.Errorf("unexpected JSON delimiter %q", delimiter) - } - return nil - } - if err := value(); err != nil { - return err - } - if _, err := decoder.Token(); !errors.Is(err, io.EOF) { - return fmt.Errorf("JSON contains trailing data") - } - return nil -} - -func (f *Fixture) Validate() error { - if f == nil || f.SchemaVersion != SchemaVersion || !identityPattern.MatchString(f.FixtureID) || !canonicalSHA256(f.MutationSeedSHA256) { - return fmt.Errorf("RQ2 source fixture identity or schema is invalid") - } - if len(f.InitialRoots) == 0 || len(f.InitialRoots) > 2 || len(f.DirectFiles)+len(f.ListFiles) == 0 || len(f.DirectFiles)+len(f.ListFiles) > maxFiles { - return fmt.Errorf("RQ2 source fixture root or file count is outside evaluator bounds") - } - roots := make(map[string]cid.Cid, len(f.InitialRoots)) - for _, binding := range f.InitialRoots { - if binding.Backend != "kzg" && binding.Backend != "ipa" || roots[binding.Backend].Defined() { - return fmt.Errorf("RQ2 source fixture has an invalid or duplicate backend root") - } - root, err := cid.Parse(binding.CID) - descriptor, _, rootErr := maltcid.ParseRoot(root) - profile, _ := BackendProfile(binding.Backend) - if err != nil || rootErr != nil || descriptor.Profile != profile || descriptor.Layout != maltcid.Prefix || descriptor.DerivationProfile != uint8(derivation.SHA256) || root.String() != binding.CID { - return fmt.Errorf("RQ2 source fixture %s root is not a canonical typed MALT CID", binding.Backend) - } - roots[binding.Backend] = root - } - paths := make(map[string]string, len(f.DirectFiles)+len(f.ListFiles)) - var decodedBytes uint64 - for _, file := range f.DirectFiles { - if err := validatePathCoordinate(file.Path, file.Coordinate); err != nil { - return fmt.Errorf("direct file: %w", err) - } - if _, exists := paths[file.Path]; exists { - return fmt.Errorf("RQ2 source fixture repeats path %q", file.Path) - } - if err := validateRawCID(file.Bytes, file.CID); err != nil { - return fmt.Errorf("direct file %q: %w", file.Path, err) - } - paths[file.Path] = "direct" - decodedBytes += uint64(len(file.Bytes)) - } - chunkCount := 0 - for _, file := range f.ListFiles { - if err := validatePathCoordinate(file.Path, file.Coordinate); err != nil { - return fmt.Errorf("list file: %w", err) - } - if _, exists := paths[file.Path]; exists { - return fmt.Errorf("RQ2 source fixture repeats path %q", file.Path) - } - if file.ChunkSize == 0 || file.TotalSize == 0 || len(file.Chunks) == 0 { - return fmt.Errorf("list file %q has an empty fixed-list descriptor", file.Path) - } - var total uint64 - for index, chunk := range file.Chunks { - if chunk.Index != uint64(index) || len(chunk.Bytes) == 0 || uint64(len(chunk.Bytes)) > file.ChunkSize { - return fmt.Errorf("list file %q chunk %d is not a bounded contiguous chunk", file.Path, index) - } - if index != len(file.Chunks)-1 && uint64(len(chunk.Bytes)) != file.ChunkSize { - return fmt.Errorf("list file %q has a partial non-final chunk", file.Path) - } - if err := validateRawCID(chunk.Bytes, chunk.CID); err != nil { - return fmt.Errorf("list file %q chunk %d: %w", file.Path, index, err) - } - total += uint64(len(chunk.Bytes)) - decodedBytes += uint64(len(chunk.Bytes)) - } - if total != file.TotalSize { - return fmt.Errorf("list file %q bytes do not match total_size", file.Path) - } - paths[file.Path] = "list" - chunkCount += len(file.Chunks) - } - if chunkCount > maxChunks || decodedBytes == 0 || decodedBytes > maxDecodedBytes { - return fmt.Errorf("RQ2 source fixture decoded bytes or chunk count is outside evaluator bounds") - } - if len(f.Operations) == 0 || len(f.Operations) > maxOperations { - return fmt.Errorf("RQ2 source fixture operation count is outside evaluator bounds") - } - operations := make(map[string]struct{}, len(f.Operations)) - for _, operation := range f.Operations { - if !identityPattern.MatchString(operation.Name) { - return fmt.Errorf("RQ2 source fixture operation name %q is invalid", operation.Name) - } - if _, exists := operations[operation.Name]; exists { - return fmt.Errorf("RQ2 source fixture repeats operation %q", operation.Name) - } - operations[operation.Name] = struct{}{} - if err := validateOperation(operation, paths); err != nil { - return fmt.Errorf("operation %q: %w", operation.Name, err) - } - if operation.Kind == KindListAppend || operation.Kind == KindListReplace { - list, ok := f.List(operation.SourcePath) - if !ok { - return fmt.Errorf("operation %q: list source is not declared", operation.Name) - } - if operation.Kind == KindListAppend && operation.PayloadBytes != list.ChunkSize { - return fmt.Errorf("operation %q: fixed-list append payload must equal chunk_size", operation.Name) - } - if operation.Kind == KindListReplace { - if operation.ListIndex == nil || *operation.ListIndex >= uint64(len(list.Chunks)) || operation.PayloadBytes != uint64(len(list.Chunks[*operation.ListIndex].Bytes)) { - return fmt.Errorf("operation %q: replacement must target one declared chunk with the same byte length", operation.Name) - } - } - } - } - return nil -} - -func validateOperation(operation Operation, initialPaths map[string]string) error { - source := func(kind string) error { - if err := validatePathCoordinate(operation.SourcePath, operation.SourceCoordinate); err != nil { - return err - } - if initialPaths[operation.SourcePath] != kind { - return fmt.Errorf("source %q is not an initial %s file", operation.SourcePath, kind) - } - return nil - } - destination := func() error { - if err := validatePathCoordinate(operation.DestinationPath, operation.DestinationCoordinate); err != nil { - return err - } - if _, exists := initialPaths[operation.DestinationPath]; exists { - return fmt.Errorf("destination %q already exists initially", operation.DestinationPath) - } - return nil - } - switch operation.Kind { - case KindDirectReplace: - if err := source("direct"); err != nil { - return err - } - return requirePayload(operation.PayloadBytes) - case KindDocumentEdit: - if err := source("direct"); err != nil { - return err - } - return requirePayload(operation.PayloadBytes) - case KindDirectInsert: - if err := destination(); err != nil { - return err - } - return requirePayload(operation.PayloadBytes) - case KindDirectDelete: - if err := source("direct"); err != nil { - return err - } - if operation.PayloadBytes != 0 { - return fmt.Errorf("delete declares payload bytes") - } - case KindDirectMove: - if err := source("direct"); err != nil { - return err - } - if err := destination(); err != nil { - return err - } - if operation.PayloadBytes != 0 { - return fmt.Errorf("move declares payload bytes") - } - case KindListAppend: - if err := source("list"); err != nil { - return err - } - if operation.ListIndex != nil { - return fmt.Errorf("append declares a list index") - } - return requirePayload(operation.PayloadBytes) - case KindListReplace: - if err := source("list"); err != nil { - return err - } - if operation.ListIndex == nil { - return fmt.Errorf("list replacement omits its exact index") - } - return requirePayload(operation.PayloadBytes) - case KindBatchInsert: - if len(operation.Batch) == 0 || len(operation.Batch) > 64 || operation.PayloadBytes != 0 { - return fmt.Errorf("batch target count or aggregate payload declaration is invalid") - } - seen := make(map[string]struct{}, len(operation.Batch)) - for _, target := range operation.Batch { - if err := validatePathCoordinate(target.Path, target.Coordinate); err != nil { - return err - } - if _, exists := initialPaths[target.Path]; exists { - return fmt.Errorf("batch destination %q already exists initially", target.Path) - } - if _, exists := seen[target.Path]; exists { - return fmt.Errorf("batch repeats destination %q", target.Path) - } - seen[target.Path] = struct{}{} - if err := requirePayload(target.PayloadBytes); err != nil { - return err - } - } - default: - return fmt.Errorf("unsupported operation kind %q", operation.Kind) - } - return nil -} - -func requirePayload(size uint64) error { - if size == 0 || size > maxDecodedBytes { - return fmt.Errorf("payload size is outside evaluator bounds") - } - return nil -} - -func validatePathCoordinate(path, coordinate string) error { - if path == "" || path != coordinate { - return fmt.Errorf("source path and authenticated coordinate must be identical and non-empty") - } - _, err := unixfs.ParseCanonicalStagedPath(path) - if err != nil { - return fmt.Errorf("path %q is not a canonical relative UnixFS path", path) - } - return nil -} - -func validateRawCID(data []byte, encoded string) error { - parsed, err := cid.Parse(encoded) - if err != nil || parsed.Type() != cid.Raw || parsed.String() != encoded { - return fmt.Errorf("CID is not one canonical raw CID") - } - expected, err := clientcas.CIDForBlock(clientcas.Block{Codec: cid.Raw, Data: data}) - if err != nil || !expected.Equals(parsed) { - return fmt.Errorf("CID does not bind the declared bytes") - } - return nil -} - -func canonicalSHA256(value string) bool { - if len(value) != sha256.Size*2 || strings.ToLower(value) != value { - return false - } - _, err := hex.DecodeString(value) - return err == nil -} - -func (f *Fixture) Root(backend string) (cid.Cid, error) { - for _, binding := range f.InitialRoots { - if binding.Backend == backend { - root, _ := cid.Parse(binding.CID) - return root, nil - } - } - return cid.Undef, fmt.Errorf("RQ2 source fixture has no %s initial root", backend) -} - -func (f *Fixture) Operation(name string) (Operation, error) { - for _, operation := range f.Operations { - if operation.Name == name { - return operation, nil - } - } - return Operation{}, fmt.Errorf("RQ2 source fixture does not declare operation %q", name) -} - -func (f *Fixture) RequireOperations(names []string) error { - want := append([]string(nil), names...) - slices.Sort(want) - want = slices.Compact(want) - for _, name := range want { - if _, err := f.Operation(name); err != nil { - return err - } - } - return nil -} - -func (f *Fixture) InitialSource() map[string][]byte { - result := make(map[string][]byte, len(f.DirectFiles)+len(f.ListFiles)) - for _, file := range f.DirectFiles { - result[file.Path] = append([]byte(nil), file.Bytes...) - } - for _, file := range f.ListFiles { - for _, chunk := range file.Chunks { - result[file.Path] = append(result[file.Path], chunk.Bytes...) - } - } - return result -} - -func (f *Fixture) List(path string) (ListFile, bool) { - for _, file := range f.ListFiles { - if file.Path == path { - return file, true - } - } - return ListFile{}, false -} - -// Payload derives the exact operation payload from the pinned mutation seed. -// This keeps repeated browser sessions bounded without letting a worker invent -// target paths or unregistered source material. -func (f *Fixture) Payload(operation string, ordinal uint64, target int, size uint64) ([]byte, error) { - if size == 0 || size > maxDecodedBytes || target < 0 || target > 64 { - return nil, fmt.Errorf("RQ2 derived payload request is outside evaluator bounds") - } - seed, err := hex.DecodeString(f.MutationSeedSHA256) - if err != nil { - return nil, err - } - label := fmt.Sprintf("%s\x00%d\x00%d", operation, ordinal, target) - base := sha256.Sum256(append(seed, []byte(label)...)) - output := make([]byte, int(size)) - for offset, counter := 0, uint64(0); offset < len(output); counter++ { - var raw [8]byte - binary.BigEndian.PutUint64(raw[:], counter) - block := sha256.Sum256(append(base[:], raw[:]...)) - offset += copy(output[offset:], block[:]) - } - return output, nil -} - -func CloneSource(source map[string][]byte) map[string][]byte { - clone := make(map[string][]byte, len(source)) - for path, data := range source { - clone[path] = append([]byte(nil), data...) - } - return clone -} - -// ApplySourceOperation returns the exact post-image and the changed payloads -// in the same order in which the operation's intent must consume their CIDs. -func (f *Fixture) ApplySourceOperation(source map[string][]byte, operation Operation, ordinal uint64) (map[string][]byte, [][]byte, error) { - post := CloneSource(source) - payload := func(index int, size uint64) ([]byte, error) { return f.Payload(operation.Name, ordinal, index, size) } - switch operation.Kind { - case KindDirectReplace: - if _, exists := post[operation.SourcePath]; !exists { - return nil, nil, fmt.Errorf("source %q does not exist", operation.SourcePath) - } - data, err := payload(0, operation.PayloadBytes) - if err != nil { - return nil, nil, err - } - post[operation.SourcePath] = data - return post, [][]byte{data}, nil - case KindDocumentEdit: - old, exists := post[operation.SourcePath] - if !exists { - return nil, nil, fmt.Errorf("document source %q does not exist", operation.SourcePath) - } - edit, err := payload(0, operation.PayloadBytes) - if err != nil { - return nil, nil, err - } - data := append(append([]byte(nil), old...), edit...) - post[operation.SourcePath] = data - return post, [][]byte{data}, nil - case KindDirectInsert: - if _, exists := post[operation.DestinationPath]; exists { - return nil, nil, fmt.Errorf("destination %q already exists", operation.DestinationPath) - } - data, err := payload(0, operation.PayloadBytes) - if err != nil { - return nil, nil, err - } - post[operation.DestinationPath] = data - return post, [][]byte{data}, nil - case KindDirectDelete: - if _, exists := post[operation.SourcePath]; !exists { - return nil, nil, fmt.Errorf("delete source %q does not exist", operation.SourcePath) - } - delete(post, operation.SourcePath) - return post, nil, nil - case KindDirectMove: - data, exists := post[operation.SourcePath] - if !exists { - return nil, nil, fmt.Errorf("move source %q does not exist", operation.SourcePath) - } - if _, exists := post[operation.DestinationPath]; exists { - return nil, nil, fmt.Errorf("move destination %q already exists", operation.DestinationPath) - } - delete(post, operation.SourcePath) - post[operation.DestinationPath] = data - return post, nil, nil - case KindListAppend: - old, exists := post[operation.SourcePath] - if !exists { - return nil, nil, fmt.Errorf("list source %q does not exist", operation.SourcePath) - } - data, err := payload(0, operation.PayloadBytes) - if err != nil { - return nil, nil, err - } - post[operation.SourcePath] = append(append([]byte(nil), old...), data...) - return post, [][]byte{data}, nil - case KindListReplace: - old, exists := post[operation.SourcePath] - list, listExists := f.List(operation.SourcePath) - if !exists || !listExists || operation.ListIndex == nil { - return nil, nil, fmt.Errorf("list replacement source is unavailable") - } - data, err := payload(0, operation.PayloadBytes) - if err != nil { - return nil, nil, err - } - offset := *operation.ListIndex * list.ChunkSize - if offset+uint64(len(data)) > uint64(len(old)) { - return nil, nil, fmt.Errorf("list replacement exceeds current source bytes") - } - old = append([]byte(nil), old...) - copy(old[offset:offset+uint64(len(data))], data) - post[operation.SourcePath] = old - return post, [][]byte{data}, nil - case KindBatchInsert: - values := make([][]byte, len(operation.Batch)) - for index, target := range operation.Batch { - if _, exists := post[target.Path]; exists { - return nil, nil, fmt.Errorf("batch destination %q already exists", target.Path) - } - data, err := payload(index, target.PayloadBytes) - if err != nil { - return nil, nil, err - } - post[target.Path], values[index] = data, data - } - return post, values, nil - default: - return nil, nil, fmt.Errorf("unsupported RQ2 source operation kind %q", operation.Kind) - } -} - -// ValidateInitialGraph proves that the fixture's declared source bytes map to -// the campaign's exact initial accepted root for the selected backend. -func (f *Fixture) ValidateInitialGraph(view authenticationgraph.View, backend string) error { - expected, err := f.Root(backend) - if err != nil { - return err - } - if !view.Root.Equals(expected) { - return fmt.Errorf("authentication graph Root %s does not equal fixture %s root %s", view.Root, backend, expected) - } - return f.ValidateGraphAgainstSource(view, backend, f.InitialSource()) -} diff --git a/internal/evaluation/rq2fixture/fixture_test.go b/internal/evaluation/rq2fixture/fixture_test.go deleted file mode 100644 index 9542425..0000000 --- a/internal/evaluation/rq2fixture/fixture_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package rq2fixture - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "testing" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - "github.com/dewebprotocol/malt-client/internal/evaluation/authenticationgraph" - "github.com/dewebprotocol/malt-core/auth/commitment/kzg" - "github.com/dewebprotocol/malt-core/derivation" - "github.com/dewebprotocol/malt-core/engine" - "github.com/dewebprotocol/malt-core/maltcid" - "github.com/dewebprotocol/malt-core/sdk/authentication" - cid "github.com/ipfs/go-cid" -) - -func TestDecodeAndOracleBindExactSource(t *testing.T) { - fixture, view, raw := testFixture(t) - if err := fixture.ValidateInitialGraph(view, "kzg"); err != nil { - t.Fatal(err) - } - hostile := fixture.InitialSource() - hostile["document.txt"][0] ^= 0xff - if err := fixture.ValidateGraphAgainstSource(view, "kzg", hostile); err == nil { - t.Fatal("source bytes unrelated to the authenticated CID passed the oracle") - } - - unknown := append(append([]byte(nil), raw[:len(raw)-1]...), []byte(`,"unknown":true}`)...) - if _, err := Decode(unknown); err == nil { - t.Fatal("unknown fixture field was accepted") - } - duplicate := []byte(`{"schema_version":"malt-rq2-source-fixture/v1","schema_version":"malt-rq2-source-fixture/v1"}`) - if _, err := Decode(duplicate); err == nil { - t.Fatal("duplicate fixture field was accepted") - } -} - -func TestDocumentEditUsesRetainedExactPostImage(t *testing.T) { - fixture, _, _ := testFixture(t) - operation, err := fixture.Operation("document-edit-cid-binding-submit") - if err != nil { - t.Fatal(err) - } - initial := fixture.InitialSource() - first, payloads, err := fixture.ApplySourceOperation(initial, operation, 0) - if err != nil || len(payloads) != 1 { - t.Fatal(err) - } - second, _, err := fixture.ApplySourceOperation(first, operation, 1) - if err != nil { - t.Fatal(err) - } - if got, want := len(second["document.txt"]), len(initial["document.txt"])+2*int(operation.PayloadBytes); got != want { - t.Fatalf("retained document bytes = %d, want %d", got, want) - } -} - -func testFixture(t *testing.T) (*Fixture, authenticationgraph.View, []byte) { - t.Helper() - data := []byte("document bytes") - key, err := clientcas.CIDForBlock(clientcas.Block{Codec: cid.Raw, Data: data}) - if err != nil { - t.Fatal(err) - } - scheme, err := kzg.NewScheme() - if err != nil { - t.Fatal(err) - } - profiles := engine.NewRegistry() - if err := profiles.Register(scheme); err != nil { - t.Fatal(err) - } - candidate, err := authentication.Prepare(context.Background(), engine.New(profiles), engine.State{Descriptor: maltcid.RootDescriptor{Layout: maltcid.Prefix, DerivationProfile: uint8(derivation.SHA256), Profile: maltcid.KZG4096}, Entries: []engine.Entry{{Label: []byte("document.txt"), Target: key}}}) - if err != nil { - t.Fatal(err) - } - root, _ := cid.Parse(candidate.Root) - view := authenticationgraph.View{Root: root, States: map[string]engine.State{root.String(): candidate.State}} - seed := sha256.Sum256([]byte("fixture seed")) - value := Fixture{ - SchemaVersion: SchemaVersion, FixtureID: "fixture", MutationSeedSHA256: hex.EncodeToString(seed[:]), - InitialRoots: []RootBinding{{Backend: "kzg", CID: root.String()}}, - DirectFiles: []DirectFile{{Path: "document.txt", Coordinate: "document.txt", Bytes: data, CID: key.String()}}, - Operations: []Operation{{ - Name: "document-edit-cid-binding-submit", Kind: KindDocumentEdit, - SourcePath: "document.txt", SourceCoordinate: "document.txt", PayloadBytes: 32, - }}, - } - raw, err := json.Marshal(value) - if err != nil { - t.Fatal(err) - } - fixture, err := Decode(raw) - if err != nil { - t.Fatal(err) - } - return fixture, view, raw -} diff --git a/internal/evaluation/rq2fixture/oracle.go b/internal/evaluation/rq2fixture/oracle.go deleted file mode 100644 index bc596c2..0000000 --- a/internal/evaluation/rq2fixture/oracle.go +++ /dev/null @@ -1,137 +0,0 @@ -package rq2fixture - -import ( - "fmt" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - "github.com/dewebprotocol/malt-client/internal/evaluation/authenticationgraph" - unixfsmodel "github.com/dewebprotocol/malt-client/unixfs/model" - "github.com/dewebprotocol/malt-core/auth/coordinate" - "github.com/dewebprotocol/malt-core/derivation" - "github.com/dewebprotocol/malt-core/engine" - "github.com/dewebprotocol/malt-core/maltcid" - cid "github.com/ipfs/go-cid" -) - -// ValidateGraphAgainstSource is an independent full post-image oracle. It -// checks every typed binding, measured sequence descriptor, chunk CID and -// reachable object against source bytes, without using the operation delta. -// The caller must first authenticate the graph using the Core SDK. -func (f *Fixture) ValidateGraphAgainstSource(view authenticationgraph.View, backend string, source map[string][]byte) error { - if f == nil { - return fmt.Errorf("RQ2 source fixture is nil") - } - profile, err := BackendProfile(backend) - if err != nil { - return err - } - descriptor, _, err := maltcid.ParseRoot(view.Root) - if err != nil || descriptor.Profile != profile || descriptor.Layout != maltcid.Prefix || descriptor.DerivationProfile != uint8(derivation.SHA256) { - return fmt.Errorf("source root requires Prefix AA1 for backend %s", backend) - } - root, err := view.State(view.Root) - if err != nil { - return err - } - if root.Descriptor != descriptor || root.PayloadCID.Defined() || len(root.Entries) != len(source) { - return fmt.Errorf("root metadata or binding count differs from source") - } - entries := make(map[string]engine.Entry, len(root.Entries)) - for _, entry := range root.Entries { - if entry.Label == nil { - return fmt.Errorf("root contains a non-label input") - } - path := string(entry.Label) - if err := validatePathCoordinate(path, path); err != nil { - return err - } - if _, duplicate := entries[path]; duplicate { - return fmt.Errorf("root repeats label %q", path) - } - entries[path] = entry - } - used := map[string]bool{view.Root.String(): true} - for path, data := range source { - entry, exists := entries[path] - if !exists { - return fmt.Errorf("source file %q is absent from authenticated root", path) - } - file, measured := f.List(path) - if !measured { - expected, err := clientcas.CIDForBlock(clientcas.Block{Codec: cid.Raw, Data: data}) - if err != nil || !entry.Target.Equals(expected) { - return fmt.Errorf("direct source bytes for %q do not bind authenticated CID", path) - } - continue - } - descriptor, _, err := maltcid.ParseRoot(entry.Target) - if err != nil || descriptor.Profile != profile || descriptor.Layout != maltcid.Positional || descriptor.DerivationProfile != uint8(derivation.Direct) { - return fmt.Errorf("source file %q requires a Positional AA0 Root for backend %s", path, backend) - } - object, err := view.State(entry.Target) - if err != nil { - return err - } - geometry, err := unixfsmodel.ParseInlineChunkMetadata(object.PayloadCID, uint64(len(object.Entries))) - if err != nil { - return err - } - if object.Descriptor != descriptor || geometry.ChunkSize != file.ChunkSize || geometry.TotalSize != uint64(len(data)) { - return fmt.Errorf("source file %q measured metadata does not match post-image", path) - } - if err := validateChunkEntries(object, data); err != nil { - return fmt.Errorf("source file %q: %w", path, err) - } - used[entry.Target.String()] = true - } - if len(view.States) != len(used) { - return fmt.Errorf("authentication graph contains %d objects, full source closure uses %d", len(view.States), len(used)) - } - for key := range view.States { - if !used[key] { - return fmt.Errorf("authentication graph contains an unrelated Root %s", key) - } - } - return nil -} -func validateChunkEntries(state engine.State, data []byte) error { - geometry, err := unixfsmodel.ParseInlineChunkMetadata(state.PayloadCID, uint64(len(state.Entries))) - if err != nil { - return err - } - count := uint64(len(data)) / geometry.ChunkSize - if uint64(len(data))%geometry.ChunkSize != 0 { - count++ - } - if uint64(len(state.Entries)) != count { - return fmt.Errorf("object has %d chunks, source bytes require %d", len(state.Entries), count) - } - // Match by explicit index; input slice order is not an authenticated property. - seen := make(map[uint64]bool, len(state.Entries)) - for _, entry := range state.Entries { - coord, decodeErr := coordinate.Parse(entry.Label) - index := coord.Index - if decodeErr != nil || coord.Kind != coordinate.Index || index >= count || seen[index] { - return fmt.Errorf("chunk input is duplicate, invalid or outside the measured sequence") - } - seen[index] = true - start := index * geometry.ChunkSize - end := start + min(uint64(len(data))-start, geometry.ChunkSize) - expected, err := clientcas.CIDForBlock(clientcas.Block{Codec: cid.Raw, Data: data[start:end]}) - if err != nil || !entry.Target.Equals(expected) { - return fmt.Errorf("chunk %d CID does not bind its source bytes", index) - } - } - return nil -} - -func BackendProfile(backend string) (maltcid.ProfileID, error) { - switch backend { - case "kzg": - return maltcid.KZG4096, nil - case "ipa": - return maltcid.IPA256, nil - default: - return 0, fmt.Errorf("unsupported backend %q", backend) - } -} diff --git a/internal/evaluation/rq2fixture/source-definition.schema.json b/internal/evaluation/rq2fixture/source-definition.schema.json deleted file mode 100644 index 2af11b8..0000000 --- a/internal/evaluation/rq2fixture/source-definition.schema.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://dewebprotocol.github.io/malt/schemas/malt-rq2-source-definition-v1.json", - "title": "MALT RQ2 source definition", - "type": "object", - "additionalProperties": false, - "required": ["schema_version", "fixture_id", "mutation_seed_sha256", "direct_files", "list_files", "operations"], - "properties": { - "schema_version": {"const": "malt-rq2-source-definition/v1"}, - "fixture_id": {"type": "string", "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$"}, - "mutation_seed_sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, - "direct_files": {"type": "array", "maxItems": 4096, "items": {"$ref": "#/$defs/direct"}}, - "list_files": {"type": "array", "maxItems": 4096, "items": {"$ref": "#/$defs/list"}}, - "operations": {"type": "array", "minItems": 1, "maxItems": 256, "items": {"oneOf": [ - {"$ref": "#/$defs/direct_replace"}, {"$ref": "#/$defs/direct_insert"}, {"$ref": "#/$defs/direct_delete"}, - {"$ref": "#/$defs/direct_move"}, {"$ref": "#/$defs/list_append"}, {"$ref": "#/$defs/list_replace"}, - {"$ref": "#/$defs/batch_insert"}, {"$ref": "#/$defs/document_edit"} - ]}} - }, - "$defs": { - "direct": { - "type": "object", "additionalProperties": false, "required": ["path", "coordinate", "bytes"], - "properties": {"path": {"type": "string", "minLength": 1}, "coordinate": {"type": "string", "minLength": 1}, "bytes": {"type": "string", "contentEncoding": "base64"}} - }, - "chunk": { - "type": "object", "additionalProperties": false, "required": ["index", "bytes"], - "properties": {"index": {"type": "integer", "minimum": 0}, "bytes": {"type": "string", "contentEncoding": "base64"}} - }, - "list": { - "type": "object", "additionalProperties": false, "required": ["path", "coordinate", "chunk_size", "total_size", "chunks"], - "properties": {"path": {"type": "string", "minLength": 1}, "coordinate": {"type": "string", "minLength": 1}, "chunk_size": {"type": "integer", "minimum": 1}, "total_size": {"type": "integer", "minimum": 1}, "chunks": {"type": "array", "minItems": 1, "maxItems": 65536, "items": {"$ref": "#/$defs/chunk"}}} - }, - "name": {"type": "string", "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$"}, - "path": {"type": "string", "minLength": 1}, - "payload_bytes": {"type": "integer", "minimum": 1, "maximum": 67108864}, - "batch_target": { - "type": "object", "additionalProperties": false, "required": ["path", "coordinate", "payload_bytes"], - "properties": {"path": {"$ref": "#/$defs/path"}, "coordinate": {"$ref": "#/$defs/path"}, "payload_bytes": {"$ref": "#/$defs/payload_bytes"}} - }, - "direct_replace": { - "type": "object", "additionalProperties": false, "required": ["name", "kind", "source_path", "source_coordinate", "payload_bytes"], - "properties": {"name": {"$ref": "#/$defs/name"}, "kind": {"const": "direct-replace"}, "source_path": {"$ref": "#/$defs/path"}, "source_coordinate": {"$ref": "#/$defs/path"}, "payload_bytes": {"$ref": "#/$defs/payload_bytes"}} - }, - "direct_insert": { - "type": "object", "additionalProperties": false, "required": ["name", "kind", "destination_path", "destination_coordinate", "payload_bytes"], - "properties": {"name": {"$ref": "#/$defs/name"}, "kind": {"const": "direct-insert"}, "destination_path": {"$ref": "#/$defs/path"}, "destination_coordinate": {"$ref": "#/$defs/path"}, "payload_bytes": {"$ref": "#/$defs/payload_bytes"}} - }, - "direct_delete": { - "type": "object", "additionalProperties": false, "required": ["name", "kind", "source_path", "source_coordinate"], - "properties": {"name": {"$ref": "#/$defs/name"}, "kind": {"const": "direct-delete"}, "source_path": {"$ref": "#/$defs/path"}, "source_coordinate": {"$ref": "#/$defs/path"}} - }, - "direct_move": { - "type": "object", "additionalProperties": false, "required": ["name", "kind", "source_path", "source_coordinate", "destination_path", "destination_coordinate"], - "properties": {"name": {"$ref": "#/$defs/name"}, "kind": {"const": "direct-move"}, "source_path": {"$ref": "#/$defs/path"}, "source_coordinate": {"$ref": "#/$defs/path"}, "destination_path": {"$ref": "#/$defs/path"}, "destination_coordinate": {"$ref": "#/$defs/path"}} - }, - "list_append": { - "type": "object", "additionalProperties": false, "required": ["name", "kind", "source_path", "source_coordinate", "payload_bytes"], - "properties": {"name": {"$ref": "#/$defs/name"}, "kind": {"const": "list-append"}, "source_path": {"$ref": "#/$defs/path"}, "source_coordinate": {"$ref": "#/$defs/path"}, "payload_bytes": {"$ref": "#/$defs/payload_bytes"}} - }, - "list_replace": { - "type": "object", "additionalProperties": false, "required": ["name", "kind", "source_path", "source_coordinate", "payload_bytes", "list_index"], - "properties": {"name": {"$ref": "#/$defs/name"}, "kind": {"const": "list-replace"}, "source_path": {"$ref": "#/$defs/path"}, "source_coordinate": {"$ref": "#/$defs/path"}, "payload_bytes": {"$ref": "#/$defs/payload_bytes"}, "list_index": {"type": "integer", "minimum": 0}} - }, - "batch_insert": { - "type": "object", "additionalProperties": false, "required": ["name", "kind", "batch"], - "properties": {"name": {"$ref": "#/$defs/name"}, "kind": {"const": "batch-insert"}, "batch": {"type": "array", "minItems": 1, "maxItems": 64, "items": {"$ref": "#/$defs/batch_target"}}} - }, - "document_edit": { - "type": "object", "additionalProperties": false, "required": ["name", "kind", "source_path", "source_coordinate", "payload_bytes"], - "properties": {"name": {"$ref": "#/$defs/name"}, "kind": {"const": "document-edit"}, "source_path": {"$ref": "#/$defs/path"}, "source_coordinate": {"$ref": "#/$defs/path"}, "payload_bytes": {"$ref": "#/$defs/payload_bytes"}} - } - } -} diff --git a/internal/evaluation/rq2fixture/source.go b/internal/evaluation/rq2fixture/source.go deleted file mode 100644 index 6436a21..0000000 --- a/internal/evaluation/rq2fixture/source.go +++ /dev/null @@ -1,183 +0,0 @@ -package rq2fixture - -import ( - "bytes" - _ "embed" - "encoding/json" - "errors" - "fmt" - "io" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - cid "github.com/ipfs/go-cid" -) - -const SourceSchemaVersion = "malt-rq2-source-definition/v1" - -//go:embed source-definition.schema.json -var SourceSchemaJSON []byte - -type SourceDirectFile struct { - Path string `json:"path"` - Coordinate string `json:"coordinate"` - Bytes []byte `json:"bytes"` -} - -type SourceListChunk struct { - Index uint64 `json:"index"` - Bytes []byte `json:"bytes"` -} - -type SourceListFile struct { - Path string `json:"path"` - Coordinate string `json:"coordinate"` - ChunkSize uint64 `json:"chunk_size"` - TotalSize uint64 `json:"total_size"` - Chunks []SourceListChunk `json:"chunks"` -} - -// SourceDefinition is the human-authored input to the production fixture -// builder. It contains source bytes and operation targets, but never roots or -// CIDs: those are independently computed for KZG and IPA by the builder. -type SourceDefinition struct { - SchemaVersion string `json:"schema_version"` - FixtureID string `json:"fixture_id"` - MutationSeedSHA256 string `json:"mutation_seed_sha256"` - DirectFiles []SourceDirectFile `json:"direct_files"` - ListFiles []SourceListFile `json:"list_files"` - Operations []Operation `json:"operations"` -} - -func DecodeSource(data []byte) (*SourceDefinition, error) { - if err := rejectDuplicateKeys(data); err != nil { - return nil, fmt.Errorf("decode RQ2 source definition: %w", err) - } - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.DisallowUnknownFields() - var source SourceDefinition - if err := decoder.Decode(&source); err != nil { - return nil, fmt.Errorf("decode RQ2 source definition: %w", err) - } - if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { - return nil, fmt.Errorf("RQ2 source definition must contain exactly one JSON object") - } - if err := source.Validate(); err != nil { - return nil, err - } - return &source, nil -} - -func (s *SourceDefinition) Validate() error { - if s == nil || s.SchemaVersion != SourceSchemaVersion || !identityPattern.MatchString(s.FixtureID) || !canonicalSHA256(s.MutationSeedSHA256) { - return fmt.Errorf("RQ2 source definition identity or schema is invalid") - } - if len(s.DirectFiles)+len(s.ListFiles) == 0 || len(s.DirectFiles)+len(s.ListFiles) > maxFiles || len(s.Operations) == 0 || len(s.Operations) > maxOperations { - return fmt.Errorf("RQ2 source definition file or operation count is outside evaluator bounds") - } - paths := make(map[string]string, len(s.DirectFiles)+len(s.ListFiles)) - var decodedBytes uint64 - for _, file := range s.DirectFiles { - if err := validatePathCoordinate(file.Path, file.Coordinate); err != nil { - return fmt.Errorf("direct file: %w", err) - } - if _, duplicate := paths[file.Path]; duplicate { - return fmt.Errorf("RQ2 source definition repeats path %q", file.Path) - } - paths[file.Path] = "direct" - decodedBytes += uint64(len(file.Bytes)) - } - chunkCount := 0 - for _, file := range s.ListFiles { - if err := validatePathCoordinate(file.Path, file.Coordinate); err != nil { - return fmt.Errorf("list file: %w", err) - } - if _, duplicate := paths[file.Path]; duplicate || file.ChunkSize == 0 || file.TotalSize == 0 || len(file.Chunks) == 0 { - return fmt.Errorf("list file %q identity or descriptor is invalid", file.Path) - } - var total uint64 - for index, chunk := range file.Chunks { - if chunk.Index != uint64(index) || len(chunk.Bytes) == 0 || uint64(len(chunk.Bytes)) > file.ChunkSize || index != len(file.Chunks)-1 && uint64(len(chunk.Bytes)) != file.ChunkSize { - return fmt.Errorf("list file %q chunk %d is not a bounded contiguous chunk", file.Path, index) - } - total += uint64(len(chunk.Bytes)) - decodedBytes += uint64(len(chunk.Bytes)) - } - if total != file.TotalSize { - return fmt.Errorf("list file %q bytes do not match total_size", file.Path) - } - paths[file.Path] = "list" - chunkCount += len(file.Chunks) - } - if decodedBytes == 0 || decodedBytes > maxDecodedBytes || chunkCount > maxChunks { - return fmt.Errorf("RQ2 source definition decoded bytes or chunks exceed evaluator bounds") - } - operations := make(map[string]struct{}, len(s.Operations)) - for _, operation := range s.Operations { - if !identityPattern.MatchString(operation.Name) { - return fmt.Errorf("RQ2 source definition operation name %q is invalid", operation.Name) - } - if _, duplicate := operations[operation.Name]; duplicate { - return fmt.Errorf("RQ2 source definition repeats operation %q", operation.Name) - } - operations[operation.Name] = struct{}{} - if err := validateOperation(operation, paths); err != nil { - return fmt.Errorf("operation %q: %w", operation.Name, err) - } - if operation.Kind == KindListAppend || operation.Kind == KindListReplace { - list, ok := s.list(operation.SourcePath) - if !ok || operation.Kind == KindListAppend && operation.PayloadBytes != list.ChunkSize { - return fmt.Errorf("operation %q does not match its fixed-list source", operation.Name) - } - if operation.Kind == KindListReplace && (operation.ListIndex == nil || *operation.ListIndex >= uint64(len(list.Chunks)) || operation.PayloadBytes != uint64(len(list.Chunks[*operation.ListIndex].Bytes))) { - return fmt.Errorf("operation %q replacement does not match its declared chunk", operation.Name) - } - } - } - return nil -} - -func (s *SourceDefinition) list(path string) (SourceListFile, bool) { - for _, file := range s.ListFiles { - if file.Path == path { - return file, true - } - } - return SourceListFile{}, false -} - -func (s *SourceDefinition) Fixture(roots []RootBinding) (*Fixture, error) { - if err := s.Validate(); err != nil { - return nil, err - } - fixture := &Fixture{ - SchemaVersion: SchemaVersion, FixtureID: s.FixtureID, MutationSeedSHA256: s.MutationSeedSHA256, - InitialRoots: append([]RootBinding(nil), roots...), Operations: append([]Operation(nil), s.Operations...), - DirectFiles: make([]DirectFile, len(s.DirectFiles)), ListFiles: make([]ListFile, len(s.ListFiles)), - } - for index, file := range s.DirectFiles { - key, err := rawCID(file.Bytes) - if err != nil { - return nil, err - } - fixture.DirectFiles[index] = DirectFile{Path: file.Path, Coordinate: file.Coordinate, Bytes: append([]byte(nil), file.Bytes...), CID: key.String()} - } - for index, file := range s.ListFiles { - result := ListFile{Path: file.Path, Coordinate: file.Coordinate, ChunkSize: file.ChunkSize, TotalSize: file.TotalSize, Chunks: make([]ListChunk, len(file.Chunks))} - for chunkIndex, chunk := range file.Chunks { - key, err := rawCID(chunk.Bytes) - if err != nil { - return nil, err - } - result.Chunks[chunkIndex] = ListChunk{Index: chunk.Index, Bytes: append([]byte(nil), chunk.Bytes...), CID: key.String()} - } - fixture.ListFiles[index] = result - } - if err := fixture.Validate(); err != nil { - return nil, err - } - return fixture, nil -} - -func rawCID(data []byte) (cid.Cid, error) { - return clientcas.CIDForBlock(clientcas.Block{Codec: cid.Raw, Data: data}) -} diff --git a/internal/evaluation/rq2fixture/source_schema_test.go b/internal/evaluation/rq2fixture/source_schema_test.go deleted file mode 100644 index bd48519..0000000 --- a/internal/evaluation/rq2fixture/source_schema_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package rq2fixture - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "testing" - - "github.com/santhosh-tekuri/jsonschema/v6" -) - -func TestSourceDefinitionSchemaMatchesStrictOperationDecoder(t *testing.T) { - schema := compileSourceSchema(t) - valid := testSourceDefinition() - raw, err := json.Marshal(valid) - if err != nil { - t.Fatal(err) - } - if _, err := DecodeSource(raw); err != nil { - t.Fatalf("Go decoder rejected valid all-kind source: %v", err) - } - if err := validateSourceSchema(schema, raw); err != nil { - t.Fatalf("schema rejected valid all-kind source: %v", err) - } - - hostile := []struct { - name string - mutate func(map[string]any) - }{ - {name: "unknown operation field", mutate: func(value map[string]any) { - operations(value)[0]["invented"] = true - }}, - {name: "missing required payload", mutate: func(value map[string]any) { - delete(operations(value)[0], "payload_bytes") - }}, - {name: "forbidden append index", mutate: func(value map[string]any) { - operations(value)[4]["list_index"] = float64(0) - }}, - {name: "unknown batch target field", mutate: func(value map[string]any) { - batch := operations(value)[6]["batch"].([]any) - batch[0].(map[string]any)["source_path"] = "wrong" - }}, - } - for _, test := range hostile { - t.Run(test.name, func(t *testing.T) { - var value map[string]any - if err := json.Unmarshal(raw, &value); err != nil { - t.Fatal(err) - } - test.mutate(value) - hostileRaw, err := json.Marshal(value) - if err != nil { - t.Fatal(err) - } - if _, err := DecodeSource(hostileRaw); err == nil { - t.Fatal("Go strict decoder accepted hostile operation shape") - } - if err := validateSourceSchema(schema, hostileRaw); err == nil { - t.Fatal("JSON schema accepted hostile operation shape") - } - }) - } -} - -func compileSourceSchema(t *testing.T) *jsonschema.Schema { - t.Helper() - compiler := jsonschema.NewCompiler() - compiler.DefaultDraft(jsonschema.Draft2020) - document, err := jsonschema.UnmarshalJSON(bytes.NewReader(SourceSchemaJSON)) - if err != nil { - t.Fatal(err) - } - if err := compiler.AddResource("source-definition.schema.json", document); err != nil { - t.Fatal(err) - } - schema, err := compiler.Compile("source-definition.schema.json") - if err != nil { - t.Fatal(err) - } - return schema -} - -func validateSourceSchema(schema *jsonschema.Schema, raw []byte) error { - instance, err := jsonschema.UnmarshalJSON(bytes.NewReader(raw)) - if err != nil { - return err - } - return schema.Validate(instance) -} - -func operations(value map[string]any) []map[string]any { - raw := value["operations"].([]any) - result := make([]map[string]any, len(raw)) - for index := range raw { - result[index] = raw[index].(map[string]any) - } - return result -} - -func testSourceDefinition() SourceDefinition { - chunkA := make([]byte, 32) - chunkB := make([]byte, 32) - copy(chunkA, "chunk-a") - copy(chunkB, "chunk-b") - seed := sha256.Sum256([]byte("source schema parity seed")) - index := uint64(0) - return SourceDefinition{ - SchemaVersion: SourceSchemaVersion, FixtureID: "fixture", MutationSeedSHA256: hex.EncodeToString(seed[:]), - DirectFiles: []SourceDirectFile{ - {Path: "document.txt", Coordinate: "document.txt", Bytes: []byte("document")}, - {Path: "delete.txt", Coordinate: "delete.txt", Bytes: []byte("delete")}, - {Path: "move.txt", Coordinate: "move.txt", Bytes: []byte("move")}, - }, - ListFiles: []SourceListFile{{ - Path: "list.bin", Coordinate: "list.bin", ChunkSize: 32, TotalSize: 64, - Chunks: []SourceListChunk{{Index: 0, Bytes: chunkA}, {Index: 1, Bytes: chunkB}}, - }}, - Operations: []Operation{ - {Name: "direct-replace", Kind: KindDirectReplace, SourcePath: "document.txt", SourceCoordinate: "document.txt", PayloadBytes: 16}, - {Name: "direct-insert", Kind: KindDirectInsert, DestinationPath: "insert.txt", DestinationCoordinate: "insert.txt", PayloadBytes: 16}, - {Name: "direct-delete", Kind: KindDirectDelete, SourcePath: "delete.txt", SourceCoordinate: "delete.txt"}, - {Name: "direct-move", Kind: KindDirectMove, SourcePath: "move.txt", SourceCoordinate: "move.txt", DestinationPath: "moved.txt", DestinationCoordinate: "moved.txt"}, - {Name: "list-append", Kind: KindListAppend, SourcePath: "list.bin", SourceCoordinate: "list.bin", PayloadBytes: 32}, - {Name: "list-replace", Kind: KindListReplace, SourcePath: "list.bin", SourceCoordinate: "list.bin", PayloadBytes: 32, ListIndex: &index}, - {Name: "batch-insert", Kind: KindBatchInsert, Batch: []BatchTarget{{Path: "batch.txt", Coordinate: "batch.txt", PayloadBytes: 16}}}, - {Name: "document-edit", Kind: KindDocumentEdit, SourcePath: "document.txt", SourceCoordinate: "document.txt", PayloadBytes: 32}, - }, - } -} - -func ExampleSourceDefinition() { - value := testSourceDefinition() - fmt.Println(value.SchemaVersion, len(value.Operations)) - // Output: malt-rq2-source-definition/v1 8 -} diff --git a/internal/evaluation/rq2metrics/contract.go b/internal/evaluation/rq2metrics/contract.go deleted file mode 100644 index 60a5c4a..0000000 --- a/internal/evaluation/rq2metrics/contract.go +++ /dev/null @@ -1,180 +0,0 @@ -// Package rq2metrics freezes the aggregation semantics of the RQ2 mutation -// metric wire contract. It deliberately treats duration taxonomy separately -// from bytes and counts: the latter are field-specific resource observations -// and are never an additive decomposition. -package rq2metrics - -import ( - "fmt" - "math" - "slices" -) - -const ( - // TaxonomyProfile is emitted by every successful RQ2 mutation record. - TaxonomyProfile = "malt-rq2-metric-taxonomy/v2" - // ResourceAggregationRule prevents reports from stacking byte/count values - // merely because their phase durations are in one exclusive wall-time set. - ResourceAggregationRule = "bytes-and-counts-field-specific-non-additive/v1" -) - -// Taxonomy is the frozen reporting contract for duration_ns. Inclusive totals -// and nested diagnostics must never be stacked with their children/parents. -// ExclusiveMutationPhases may be stacked under mutation_total only; the -// residual is intentionally unclassified operation work. BrowserBoundaryPhases -// are exclusive only under the browser host call, outside mutation_total. -// OrthogonalResources use a different resource domain (CPU or memory). -type Taxonomy struct { - InclusiveTotals []string - ExclusiveMutationPhases []string - NestedDiagnostics []string - ColdStartupPhases []string - BrowserBoundaryPhases []string - OrthogonalResources []string - ResourceAggregationProfile string -} - -var contract = Taxonomy{ - InclusiveTotals: []string{"mutation_total", "candidate_generation", "first_mutation"}, - ExclusiveMutationPhases: []string{ - "scan", "chunk", "hash", "graph_snapshot", - "candidate_apply", "candidate_export", "batch_encoding", "upload", "receipt_check", - }, - NestedDiagnostics: []string{"gateway_validate_stage", "gateway_persist"}, - ColdStartupPhases: []string{"wasm_download", "wasm_instantiate", "parameter_load"}, - BrowserBoundaryPhases: []string{"js_wasm_boundary"}, - OrthogonalResources: []string{"cpu_total", "peak_memory"}, - ResourceAggregationProfile: ResourceAggregationRule, -} - -// Contract returns an independent copy so tests and report adapters can bind -// the exact classification without mutating the process-global definition. -func Contract() Taxonomy { - return Taxonomy{ - InclusiveTotals: slices.Clone(contract.InclusiveTotals), - ExclusiveMutationPhases: slices.Clone(contract.ExclusiveMutationPhases), - NestedDiagnostics: slices.Clone(contract.NestedDiagnostics), - ColdStartupPhases: slices.Clone(contract.ColdStartupPhases), - BrowserBoundaryPhases: slices.Clone(contract.BrowserBoundaryPhases), - OrthogonalResources: slices.Clone(contract.OrthogonalResources), - ResourceAggregationProfile: contract.ResourceAggregationProfile, - } -} - -// Observation contains only the wall-duration attributes needed for -// reconciliation. Applicability is included so operation-specific omitted -// phases (for example delete payload upload) contribute no synthetic zero. -type Observation struct { - Applicable bool - DurationNS uint64 -} - -// Validate reconciles the overlapping duration domains. It is intentionally -// fail-closed: overflow or a child/subset exceeding its containing measurement -// invalidates the record instead of being hidden in a paper residual. -func Validate(profile string, values map[string]Observation, browser, coldMutation bool) error { - if profile != TaxonomyProfile { - return fmt.Errorf("RQ2 metric taxonomy profile %q is not %q", profile, TaxonomyProfile) - } - for _, name := range allMetricNames() { - if _, ok := values[name]; !ok { - return fmt.Errorf("RQ2 metric taxonomy is missing %q", name) - } - } - total := values["mutation_total"] - clientRoot := values["candidate_generation"] - if !total.Applicable || !clientRoot.Applicable || total.DurationNS == 0 || clientRoot.DurationNS == 0 { - return fmt.Errorf("RQ2 inclusive mutation/client-root totals are absent") - } - if clientRoot.DurationNS > total.DurationNS { - return fmt.Errorf("candidate_generation exceeds inclusive mutation_total") - } - exclusive, err := sumApplicable(values, contract.ExclusiveMutationPhases) - if err != nil { - return err - } - if exclusive > total.DurationNS { - return fmt.Errorf("exclusive mutation phases exceed inclusive mutation_total") - } - sdkExclusive, err := sumApplicable(values, []string{"graph_snapshot", "candidate_apply", "candidate_export"}) - if err != nil { - return err - } - if sdkExclusive > clientRoot.DurationNS { - return fmt.Errorf("exclusive SDK phases exceed candidate_generation") - } - gateway, err := sumApplicable(values, []string{"gateway_validate_stage", "gateway_persist"}) - if err != nil { - return err - } - if gateway > total.DurationNS { - return fmt.Errorf("nested Gateway diagnostics exceed inclusive mutation_total") - } - first := values["first_mutation"] - boundary := values["js_wasm_boundary"] - if browser { - if !boundary.Applicable { - return fmt.Errorf("browser metric taxonomy omits js_wasm_boundary") - } - if first.Applicable != coldMutation { - return fmt.Errorf("browser first_mutation applicability=%t, want %t", first.Applicable, coldMutation) - } - if coldMutation { - combined, err := add(total.DurationNS, boundary.DurationNS) - if err != nil { - return err - } - if combined > first.DurationNS { - return fmt.Errorf("mutation_total plus JS/WASM boundary exceeds first_mutation host call") - } - } - } else if first.Applicable || boundary.Applicable { - return fmt.Errorf("native metric taxonomy contains browser host-call durations") - } - return nil -} - -func allMetricNames() []string { - result := make([]string, 0, len(contract.InclusiveTotals)+len(contract.ExclusiveMutationPhases)+len(contract.NestedDiagnostics)+len(contract.ColdStartupPhases)+len(contract.BrowserBoundaryPhases)+len(contract.OrthogonalResources)) - for _, names := range [][]string{ - contract.InclusiveTotals, contract.ExclusiveMutationPhases, contract.NestedDiagnostics, - contract.ColdStartupPhases, contract.BrowserBoundaryPhases, contract.OrthogonalResources, - } { - result = append(result, names...) - } - return result -} - -func sumApplicable(values map[string]Observation, names []string) (uint64, error) { - var total uint64 - for _, name := range names { - value := values[name] - if !value.Applicable { - continue - } - var err error - total, err = add(total, value.DurationNS) - if err != nil { - return 0, fmt.Errorf("RQ2 duration reconciliation for %q: %w", name, err) - } - } - return total, nil -} - -func add(left, right uint64) (uint64, error) { - return AddDurations(left, right) -} - -// AddDurations performs the same checked aggregation used by the executable -// taxonomy contract. Worker adapters use it when extending an inclusive SDK -// subtotal with caller-owned canonicalization phases. -func AddDurations(values ...uint64) (uint64, error) { - var total uint64 - for _, value := range values { - if math.MaxUint64-total < value { - return 0, fmt.Errorf("duration sum overflows uint64") - } - total += value - } - return total, nil -} diff --git a/internal/evaluation/rq2metrics/contract_test.go b/internal/evaluation/rq2metrics/contract_test.go deleted file mode 100644 index 14ffdce..0000000 --- a/internal/evaluation/rq2metrics/contract_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package rq2metrics - -import ( - "math" - "reflect" - "strings" - "testing" -) - -func TestTaxonomyContractIsFrozenAndDefensivelyCopied(t *testing.T) { - got := Contract() - want := Taxonomy{ - InclusiveTotals: []string{"mutation_total", "candidate_generation", "first_mutation"}, - ExclusiveMutationPhases: []string{ - "scan", "chunk", "hash", "graph_snapshot", - "candidate_apply", "candidate_export", "batch_encoding", "upload", "receipt_check", - }, - NestedDiagnostics: []string{"gateway_validate_stage", "gateway_persist"}, - ColdStartupPhases: []string{"wasm_download", "wasm_instantiate", "parameter_load"}, - BrowserBoundaryPhases: []string{"js_wasm_boundary"}, - OrthogonalResources: []string{"cpu_total", "peak_memory"}, - ResourceAggregationProfile: ResourceAggregationRule, - } - if !reflect.DeepEqual(got, want) { - t.Fatalf("taxonomy = %#v, want %#v", got, want) - } - got.InclusiveTotals[0] = "changed" - if Contract().InclusiveTotals[0] != "mutation_total" { - t.Fatal("caller mutated the frozen taxonomy") - } -} - -func TestValidateReconcilesDurationDomains(t *testing.T) { - values := validObservations(true, true) - if err := Validate(TaxonomyProfile, values, true, true); err != nil { - t.Fatal(err) - } - tests := map[string]func(map[string]Observation){ - "exclusive exceeds mutation total": func(value map[string]Observation) { - value["scan"] = Observation{Applicable: true, DurationNS: 101} - }, - "sdk phases exceed subtotal": func(value map[string]Observation) { - value["candidate_generation"] = Observation{Applicable: true, DurationNS: 5} - }, - "gateway diagnostics exceed total": func(value map[string]Observation) { - value["gateway_validate_stage"] = Observation{Applicable: true, DurationNS: 60} - value["gateway_persist"] = Observation{Applicable: true, DurationNS: 60} - }, - "browser call cannot contain mutation and boundary": func(value map[string]Observation) { - value["first_mutation"] = Observation{Applicable: true, DurationNS: 100} - value["js_wasm_boundary"] = Observation{Applicable: true, DurationNS: 1} - }, - "sum overflow": func(value map[string]Observation) { - value["mutation_total"] = Observation{Applicable: true, DurationNS: math.MaxUint64} - value["scan"] = Observation{Applicable: true, DurationNS: math.MaxUint64} - value["chunk"] = Observation{Applicable: true, DurationNS: 1} - }, - } - for name, mutate := range tests { - t.Run(name, func(t *testing.T) { - copy := cloneObservations(values) - mutate(copy) - if err := Validate(TaxonomyProfile, copy, true, true); err == nil { - t.Fatal("invalid overlapping durations were accepted") - } - }) - } -} - -func TestValidateRejectsProfileApplicabilityAndMissingMetricDrift(t *testing.T) { - values := validObservations(false, false) - if err := Validate(TaxonomyProfile, values, false, false); err != nil { - t.Fatal(err) - } - if err := Validate("stale", values, false, false); err == nil || !strings.Contains(err.Error(), "profile") { - t.Fatalf("profile error = %v", err) - } - delete(values, "peak_memory") - if err := Validate(TaxonomyProfile, values, false, false); err == nil || !strings.Contains(err.Error(), "missing") { - t.Fatalf("missing-metric error = %v", err) - } -} - -func TestAddDurationsIsExactAndFailsClosedOnOverflow(t *testing.T) { - if total, err := AddDurations(1, 2, 3, 4); err != nil || total != 10 { - t.Fatalf("AddDurations = %d, %v; want 10", total, err) - } - if _, err := AddDurations(math.MaxUint64-1, 1, 1); err == nil { - t.Fatal("duration aggregation overflow was accepted") - } -} - -func validObservations(browser, cold bool) map[string]Observation { - values := make(map[string]Observation) - for _, name := range allMetricNames() { - values[name] = Observation{} - } - values["mutation_total"] = Observation{Applicable: true, DurationNS: 100} - values["candidate_generation"] = Observation{Applicable: true, DurationNS: 30} - for _, name := range []string{"scan", "chunk", "hash", "graph_snapshot", "candidate_apply", "candidate_export", "batch_encoding", "upload", "receipt_check"} { - values[name] = Observation{Applicable: true, DurationNS: 2} - } - values["gateway_validate_stage"] = Observation{Applicable: true, DurationNS: 2} - values["gateway_persist"] = Observation{Applicable: true, DurationNS: 2} - values["cpu_total"] = Observation{Applicable: true, DurationNS: 150} - values["peak_memory"] = Observation{Applicable: true, DurationNS: 100} - if browser { - values["js_wasm_boundary"] = Observation{Applicable: true, DurationNS: 5} - if cold { - values["first_mutation"] = Observation{Applicable: true, DurationNS: 110} - for _, name := range []string{"wasm_download", "wasm_instantiate", "parameter_load"} { - values[name] = Observation{Applicable: true, DurationNS: 2} - } - } - } - return values -} - -func cloneObservations(source map[string]Observation) map[string]Observation { - result := make(map[string]Observation, len(source)) - for name, value := range source { - result[name] = value - } - return result -} diff --git a/internal/evaluation/rq2wire/wire.go b/internal/evaluation/rq2wire/wire.go deleted file mode 100644 index 4b60c4e..0000000 --- a/internal/evaluation/rq2wire/wire.go +++ /dev/null @@ -1,465 +0,0 @@ -// Package rq2wire mirrors the evaluator-owned RQ2 JSONL contract without -// importing malt-evaluation. It is shared only by the browser host adapter and -// the real Go/WASM writer artifact built from this repository. -package rq2wire - -import ( - "encoding/hex" - "fmt" - "regexp" - "slices" - "strings" - - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2metrics" - "github.com/dewebprotocol/malt-core/maltcid" - cid "github.com/ipfs/go-cid" -) - -const ( - WorkerRequestSchema = "malt-rq2-worker-request/v1" - WorkerRecordSchema = "malt-rq2-worker-record/v2" - - ClientNative = "native" - ClientBrowserWASM = "browser-wasm" - - LifecycleNativeLong = "native-long-lived" - LifecycleBrowserCold = "browser-cold" - LifecycleBrowserSteady = "browser-steady" - LifecycleBrowserShort = "browser-short-session" - - RecordPreflight = "preflight" - RecordSessionStart = "session-start" - RecordMutation = "mutation" - RecordSessionEnd = "session-end" - - PhaseObserved = "observed" - PhaseNotApplicable = "not-applicable" -) - -var IDPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$`) - -type WorkerRequest struct { - SchemaVersion string `json:"schema_version"` - WorkerID string `json:"worker_id"` - RequestID string `json:"request_id"` - RecordKind string `json:"record_kind"` - SessionID string `json:"session_id"` - ClientKind string `json:"client_kind"` - PlatformID string `json:"platform_id"` - Backend string `json:"backend"` - Lifecycle string `json:"lifecycle"` - FixtureID string `json:"fixture_id"` - Operation string `json:"operation,omitempty"` - Measured bool `json:"measured"` - ExpectedAcceptedRoot string `json:"expected_accepted_root,omitempty"` -} - -func (r WorkerRequest) Validate() error { - if r.SchemaVersion != WorkerRequestSchema || !IDPattern.MatchString(r.WorkerID) || !IDPattern.MatchString(r.RequestID) || - !IDPattern.MatchString(r.SessionID) || !IDPattern.MatchString(r.PlatformID) || !IDPattern.MatchString(r.FixtureID) { - return fmt.Errorf("invalid RQ2 worker request identity") - } - if err := ValidateCoordinate(r.ClientKind, r.Backend, r.Lifecycle); err != nil { - return err - } - switch r.RecordKind { - case RecordPreflight: - if r.Operation != "" || r.Measured || r.ExpectedAcceptedRoot != "" { - return fmt.Errorf("preflight request contains mutation/session state") - } - case RecordSessionStart, RecordSessionEnd: - if r.Operation != "" || r.Measured || !ValidTypedRoot(r.ExpectedAcceptedRoot, r.Backend) { - return fmt.Errorf("session request must bind one typed accepted root") - } - case RecordMutation: - if !IDPattern.MatchString(r.Operation) || !ValidTypedRoot(r.ExpectedAcceptedRoot, r.Backend) { - return fmt.Errorf("mutation request is incomplete") - } - default: - return fmt.Errorf("unsupported RQ2 worker request kind %q", r.RecordKind) - } - return nil -} - -type RuntimeEvidence struct { - OS string `json:"os"` - Architecture string `json:"architecture"` - LowPowerARM bool `json:"low_power_arm"` - MachineDescriptorID string `json:"machine_descriptor_id"` - MachineDescriptorSHA256 string `json:"machine_descriptor_sha256"` - BrowserEngine string `json:"browser_engine,omitempty"` - WASMSHA256 string `json:"wasm_sha256,omitempty"` - ParameterProfile string `json:"parameter_profile,omitempty"` - ParameterSHA256 string `json:"parameter_sha256,omitempty"` - ParameterInputBytes uint64 `json:"parameter_input_bytes,omitempty"` -} - -func ParameterEvidence(backend string) (profile, digest string, inputBytes uint64, ok bool) { - switch backend { - case "kzg": - // Core NewScheme consumes its 192-byte opening key and 393216-byte - // Lagrange writer key. Digest order is opening key, then writer key. - return "malt-kzg4096-binary-opening-then-writer-key/v1", "5ce50c7a06499dee8b17987d594e5552232d9bd7a5ea8c5f1b29e951717f0c9d", 393408, true - case "ipa": - // go-ipa GenerateRandomPoints consumes this exact 19-byte seed; - // VectorLength=256 is fixed code/configuration rather than loaded bytes. - return "go-ipa-53bbb0ceb27a-seed-eth-verkle-oct-2021", "08c26364c812dffb5ba046479f25ca9afdc8ab39b0c89f37506cab988a96ac21", 19, true - default: - return "", "", 0, false - } -} - -type SessionEvidence struct { - GraphFetch PhaseMeasurement `json:"graph_fetch"` - GraphImport PhaseMeasurement `json:"graph_import"` - AcceptedRoot string `json:"accepted_root"` - ReceiptCount uint64 `json:"receipt_count"` - AuditPassed bool `json:"audit_passed"` -} - -type PhaseMeasurement struct { - Applicable bool `json:"applicable"` - Status string `json:"status"` - DurationNS uint64 `json:"duration_ns"` - Bytes uint64 `json:"bytes"` - Count uint64 `json:"count"` -} - -func ObservedPhase(duration, bytes, count uint64) PhaseMeasurement { - return PhaseMeasurement{Applicable: true, Status: PhaseObserved, DurationNS: duration, Bytes: bytes, Count: count} -} - -func NotApplicablePhase() PhaseMeasurement { - return PhaseMeasurement{Status: PhaseNotApplicable} -} - -func (p PhaseMeasurement) validate(name string, required bool) error { - if p.Applicable != required { - return fmt.Errorf("phase %s applicable=%t, want %t", name, p.Applicable, required) - } - if !p.Applicable { - if p.Status != PhaseNotApplicable { - return fmt.Errorf("non-applicable phase %s has invalid status %q", name, p.Status) - } - if p.DurationNS != 0 || p.Bytes != 0 || p.Count != 0 { - return fmt.Errorf("non-applicable phase %s has measurements", name) - } - return nil - } - if p.Status != PhaseObserved { - return fmt.Errorf("applicable phase %s has status %q", name, p.Status) - } - if p.Count == 0 { - return fmt.Errorf("applicable phase %s has no observation count", name) - } - return nil -} - -type MutationMetrics struct { - // TaxonomyProfile binds the executable aggregation contract. Duration - // classes are frozen in rq2metrics; bytes/counts remain field-specific, - // non-additive resource observations regardless of duration class. - TaxonomyProfile string `json:"taxonomy_profile"` - MutationTotal PhaseMeasurement `json:"mutation_total"` - Scan PhaseMeasurement `json:"scan"` - Chunk PhaseMeasurement `json:"chunk"` - Hash PhaseMeasurement `json:"hash"` - - GraphSnapshot PhaseMeasurement `json:"graph_snapshot"` - - CandidateExport PhaseMeasurement `json:"candidate_export"` - CandidateApply PhaseMeasurement `json:"candidate_apply"` - CandidateGeneration PhaseMeasurement `json:"candidate_generation"` - BatchEncoding PhaseMeasurement `json:"batch_encoding"` - Upload PhaseMeasurement `json:"upload"` - GatewayValidateStage PhaseMeasurement `json:"gateway_validate_stage"` - GatewayPersist PhaseMeasurement `json:"gateway_persist"` - ReceiptCheck PhaseMeasurement `json:"receipt_check"` - CPUTotal PhaseMeasurement `json:"cpu_total"` - PeakMemory PhaseMeasurement `json:"peak_memory"` - WASMDownload PhaseMeasurement `json:"wasm_download"` - WASMInstantiate PhaseMeasurement `json:"wasm_instantiate"` - ParameterLoad PhaseMeasurement `json:"parameter_load"` - FirstMutation PhaseMeasurement `json:"first_mutation"` - JSWASMBoundary PhaseMeasurement `json:"js_wasm_boundary"` -} - -func (m MutationMetrics) Validate(clientKind, backend, lifecycle, operation string) error { - if m.TaxonomyProfile != rq2metrics.TaxonomyProfile { - return fmt.Errorf("invalid RQ2 metric taxonomy profile %q", m.TaxonomyProfile) - } - scanChunkRequired, hashRequired := pipelineApplicability(clientKind, operation) - for _, phase := range []struct { - name string - value PhaseMeasurement - required bool - }{{"scan", m.Scan, scanChunkRequired}, {"chunk", m.Chunk, scanChunkRequired}, {"hash", m.Hash, hashRequired}} { - if err := phase.value.validate(phase.name, phase.required); err != nil { - return err - } - } - for _, phase := range []struct { - name string - value PhaseMeasurement - }{ - {"mutation_total", m.MutationTotal}, {"graph_snapshot", m.GraphSnapshot}, - {"candidate_export", m.CandidateExport}, {"candidate_apply", m.CandidateApply}, - {"candidate_generation", m.CandidateGeneration}, {"batch_encoding", m.BatchEncoding}, - {"gateway_validate_stage", m.GatewayValidateStage}, {"gateway_persist", m.GatewayPersist}, {"receipt_check", m.ReceiptCheck}, - {"cpu_total", m.CPUTotal}, {"peak_memory", m.PeakMemory}, - } { - if err := phase.value.validate(phase.name, true); err != nil { - return err - } - } - uploadRequired := clientKind == ClientBrowserWASM || clientKind == ClientNative && - operation != "delete-directory-entry" && operation != "move" && operation != "rename" - if err := m.Upload.validate("upload", uploadRequired); err != nil { - return err - } - if m.MutationTotal.DurationNS == 0 || m.BatchEncoding.Bytes == 0 || - m.ReceiptCheck.Bytes == 0 || m.PeakMemory.Bytes == 0 || uploadRequired && m.Upload.Bytes == 0 { - return fmt.Errorf("total latency and directional batch/upload/receipt/memory accounting must be nonzero") - } - browser := clientKind == ClientBrowserWASM - cold := lifecycle == LifecycleBrowserCold - short := lifecycle == LifecycleBrowserShort - coldObserved := cold || short && m.WASMDownload.Applicable - for _, phase := range []struct { - name string - value PhaseMeasurement - required bool - }{ - {"wasm_download", m.WASMDownload, coldObserved}, {"wasm_instantiate", m.WASMInstantiate, coldObserved}, - {"parameter_load", m.ParameterLoad, coldObserved}, {"first_mutation", m.FirstMutation, coldObserved}, - {"js_wasm_boundary", m.JSWASMBoundary, browser}, - } { - if err := phase.value.validate(phase.name, phase.required); err != nil { - return err - } - } - if coldObserved && (m.WASMDownload.Bytes == 0 || m.ParameterLoad.Bytes == 0) { - return fmt.Errorf("cold WASM download and parameter byte accounting must be nonzero") - } - if coldObserved { - _, _, inputBytes, ok := ParameterEvidence(backend) - if !ok || m.ParameterLoad.Bytes != inputBytes { - return fmt.Errorf("cold parameter-load bytes do not match the exact backend initialization input") - } - } - return rq2metrics.Validate(m.TaxonomyProfile, m.durationObservations(), browser, coldObserved) -} - -func pipelineApplicability(clientKind, operation string) (scanChunk, hash bool) { - nativePayload := clientKind == ClientNative && operation != "delete-directory-entry" && operation != "move" && operation != "rename" - return operation == "document-edit-cid-binding-submit" || nativePayload, clientKind == ClientBrowserWASM || nativePayload -} - -func (m MutationMetrics) durationObservations() map[string]rq2metrics.Observation { - result := make(map[string]rq2metrics.Observation, 23) - for _, metric := range []struct { - name string - value PhaseMeasurement - }{ - {"mutation_total", m.MutationTotal}, {"scan", m.Scan}, {"chunk", m.Chunk}, {"hash", m.Hash}, - {"graph_snapshot", m.GraphSnapshot}, - {"candidate_export", m.CandidateExport}, - {"candidate_apply", m.CandidateApply}, {"candidate_generation", m.CandidateGeneration}, - {"batch_encoding", m.BatchEncoding}, {"upload", m.Upload}, {"gateway_validate_stage", m.GatewayValidateStage}, - {"gateway_persist", m.GatewayPersist}, {"receipt_check", m.ReceiptCheck}, {"cpu_total", m.CPUTotal}, - {"peak_memory", m.PeakMemory}, {"wasm_download", m.WASMDownload}, {"wasm_instantiate", m.WASMInstantiate}, - {"parameter_load", m.ParameterLoad}, {"first_mutation", m.FirstMutation}, {"js_wasm_boundary", m.JSWASMBoundary}, - } { - result[metric.name] = rq2metrics.Observation{Applicable: metric.value.Applicable, DurationNS: metric.value.DurationNS} - } - return result -} - -type MutationEvidence struct { - Operation string `json:"operation"` - PriorRoot string `json:"prior_root"` - CandidateRoot string `json:"candidate_root"` - ReceiptRoot string `json:"receipt_root"` - ReceiptAccepted bool `json:"receipt_accepted"` - - BatchSHA256 string `json:"batch_sha256"` - Metrics MutationMetrics `json:"metrics"` -} - -type WorkerRecord struct { - SchemaVersion string `json:"schema_version"` - WorkerID string `json:"worker_id"` - RequestID string `json:"request_id"` - RecordKind string `json:"record_kind"` - SessionID string `json:"session_id"` - ClientKind string `json:"client_kind"` - PlatformID string `json:"platform_id"` - Backend string `json:"backend"` - Lifecycle string `json:"lifecycle"` - FixtureID string `json:"fixture_id"` - Measured bool `json:"measured"` - Success bool `json:"success"` - FailureClass string `json:"failure_class,omitempty"` - Error string `json:"error,omitempty"` - Capabilities []string `json:"capabilities,omitempty"` - Runtime *RuntimeEvidence `json:"runtime,omitempty"` - Session *SessionEvidence `json:"session,omitempty"` - Mutation *MutationEvidence `json:"mutation,omitempty"` -} - -func BaseRecord(request WorkerRequest) WorkerRecord { - return WorkerRecord{ - SchemaVersion: WorkerRecordSchema, WorkerID: request.WorkerID, RequestID: request.RequestID, - RecordKind: request.RecordKind, SessionID: request.SessionID, ClientKind: request.ClientKind, - PlatformID: request.PlatformID, Backend: request.Backend, Lifecycle: request.Lifecycle, - FixtureID: request.FixtureID, Measured: request.Measured, - } -} - -func FailedRecord(request WorkerRequest, class string, err error) WorkerRecord { - record := BaseRecord(request) - record.FailureClass, record.Error = class, err.Error() - return record -} - -func (r WorkerRecord) Validate() error { - if !slices.Contains([]string{RecordPreflight, RecordSessionStart, RecordMutation, RecordSessionEnd}, r.RecordKind) { - return fmt.Errorf("unsupported RQ2 worker record kind %q", r.RecordKind) - } - if !IDPattern.MatchString(r.WorkerID) || !IDPattern.MatchString(r.RequestID) || !IDPattern.MatchString(r.SessionID) || - !IDPattern.MatchString(r.PlatformID) || !IDPattern.MatchString(r.FixtureID) || r.SchemaVersion != WorkerRecordSchema { - return fmt.Errorf("invalid RQ2 worker record identity") - } - if err := ValidateCoordinate(r.ClientKind, r.Backend, r.Lifecycle); err != nil { - return err - } - if !r.Success { - if r.FailureClass == "" || r.Error == "" || r.Capabilities != nil || r.Runtime != nil || r.Session != nil || r.Mutation != nil { - return fmt.Errorf("failed RQ2 worker record contains invalid evidence") - } - return nil - } - if r.FailureClass != "" || r.Error != "" { - return fmt.Errorf("successful RQ2 worker record has failure metadata") - } - switch r.RecordKind { - case RecordPreflight: - if r.Measured || r.Runtime == nil || r.Session != nil || r.Mutation != nil || !slices.Equal(r.Capabilities, RequiredCapabilities(r.ClientKind, r.Backend)) { - return fmt.Errorf("invalid RQ2 preflight evidence") - } - if !RuntimeIdentityValid(*r.Runtime) { - return fmt.Errorf("RQ2 preflight machine descriptor provenance is incomplete") - } - if r.ClientKind == ClientBrowserWASM { - profile, digest, inputBytes, ok := ParameterEvidence(r.Backend) - if !ok || r.Runtime.ParameterProfile != profile || r.Runtime.ParameterSHA256 != digest || r.Runtime.ParameterInputBytes != inputBytes { - return fmt.Errorf("browser preflight parameter provenance is not exact") - } - } else if r.Runtime.ParameterProfile != "" || r.Runtime.ParameterSHA256 != "" || r.Runtime.ParameterInputBytes != 0 { - return fmt.Errorf("native preflight must not claim browser parameter-load provenance") - } - case RecordSessionStart: - if r.Session == nil || r.Session.GraphFetch.validate("graph_fetch", true) != nil || r.Session.GraphImport.validate("graph_import", true) != nil || r.Session.GraphFetch.Bytes == 0 || r.Session.GraphImport.Count == 0 { - return fmt.Errorf("session-start omits authenticated graph import") - } - if r.Measured || r.Session == nil || r.Mutation != nil || r.Runtime != nil || r.Capabilities != nil || - !ValidTypedRoot(r.Session.AcceptedRoot, r.Backend) || r.Session.ReceiptCount != 0 || r.Session.AuditPassed { - return fmt.Errorf("invalid RQ2 session-start evidence") - } - case RecordMutation: - if r.Mutation == nil || r.Session != nil || r.Runtime != nil || r.Capabilities != nil { - return fmt.Errorf("invalid RQ2 mutation evidence") - } - if !IDPattern.MatchString(r.Mutation.Operation) || !ValidTypedRoot(r.Mutation.PriorRoot, r.Backend) || - !ValidTypedRoot(r.Mutation.CandidateRoot, r.Backend) || r.Mutation.CandidateRoot != r.Mutation.ReceiptRoot || !r.Mutation.ReceiptAccepted { - return fmt.Errorf("invalid RQ2 mutation roots") - } - for _, digest := range []string{r.Mutation.BatchSHA256} { - if !CanonicalSHA256(digest) { - return fmt.Errorf("invalid RQ2 mutation digest") - } - } - return r.Mutation.Metrics.Validate(r.ClientKind, r.Backend, r.Lifecycle, r.Mutation.Operation) - case RecordSessionEnd: - if r.Session == nil || r.Session.GraphFetch.validate("graph_fetch", false) != nil || r.Session.GraphImport.validate("graph_import", false) != nil { - return fmt.Errorf("session-end carries startup graph import") - } - if r.Measured || r.Session == nil || r.Mutation != nil || r.Runtime != nil || r.Capabilities != nil || - !ValidTypedRoot(r.Session.AcceptedRoot, r.Backend) || !r.Session.AuditPassed { - return fmt.Errorf("invalid RQ2 session-end evidence") - } - default: - return fmt.Errorf("unsupported RQ2 record kind %q", r.RecordKind) - } - return nil -} - -func RequiredCapabilities(clientKind, backend string) []string { - values := []string{"authentication-batch-v0", "authentication-candidate-v0", "exact-authentication-receipt-v0", "phase-metrics-v2", "retained-authentication-writer-v0"} - if clientKind == ClientNative { - values = append(values, "long-lived-session-v1", "native-writer-v1") - } else if clientKind == ClientBrowserWASM { - values = append(values, "wasm-cold-start-v1", "wasm-steady-session-v1", "wasm-writer-v1") - } - values = append(values, "commitment-"+backend+"-v1") - slices.Sort(values) - return values -} - -func ValidateCoordinate(clientKind, backend, lifecycle string) error { - if backend != "kzg" && backend != "ipa" { - return fmt.Errorf("unsupported RQ2 backend %q", backend) - } - switch clientKind { - case ClientNative: - if lifecycle != LifecycleNativeLong { - return fmt.Errorf("native client requires long-lived lifecycle") - } - case ClientBrowserWASM: - if lifecycle != LifecycleBrowserCold && lifecycle != LifecycleBrowserSteady && lifecycle != LifecycleBrowserShort { - return fmt.Errorf("browser client requires cold, steady, or short-session lifecycle") - } - default: - return fmt.Errorf("unsupported RQ2 client kind %q", clientKind) - } - return nil -} - -func ValidTypedRoot(value, backend string) bool { - parsed, err := cid.Parse(value) - descriptor, _, rootErr := maltcid.ParseRoot(parsed) - profile := maltcid.KZG4096 - if backend == "ipa" { - profile = maltcid.IPA256 - } - return err == nil && rootErr == nil && parsed.String() == value && descriptor.Profile == profile && (backend == "kzg" || backend == "ipa") -} - -func CanonicalSHA256(value string) bool { - if len(value) != 64 { - return false - } - decoded, err := hex.DecodeString(value) - return err == nil && hex.EncodeToString(decoded) == value -} - -func Bind(record WorkerRecord, request WorkerRequest) error { - if record.WorkerID != request.WorkerID || record.RequestID != request.RequestID || record.RecordKind != request.RecordKind || - record.SessionID != request.SessionID || record.ClientKind != request.ClientKind || record.PlatformID != request.PlatformID || - record.Backend != request.Backend || record.Lifecycle != request.Lifecycle || record.FixtureID != request.FixtureID || record.Measured != request.Measured { - return fmt.Errorf("RQ2 worker response does not bind request %q", request.RequestID) - } - if record.Success && request.RecordKind == RecordMutation && (record.Mutation == nil || record.Mutation.Operation != request.Operation || record.Mutation.PriorRoot != request.ExpectedAcceptedRoot) { - return fmt.Errorf("RQ2 mutation response does not bind operation/prior root") - } - if record.Success && (request.RecordKind == RecordSessionStart || request.RecordKind == RecordSessionEnd) && - (record.Session == nil || record.Session.AcceptedRoot != request.ExpectedAcceptedRoot) { - return fmt.Errorf("RQ2 session response does not bind expected root") - } - return nil -} - -func RuntimeIdentityValid(runtime RuntimeEvidence) bool { - return strings.TrimSpace(runtime.OS) != "" && strings.TrimSpace(runtime.Architecture) != "" && - IDPattern.MatchString(runtime.MachineDescriptorID) && CanonicalSHA256(runtime.MachineDescriptorSHA256) -} diff --git a/internal/evaluation/rq2wire/wire_test.go b/internal/evaluation/rq2wire/wire_test.go deleted file mode 100644 index dce0074..0000000 --- a/internal/evaluation/rq2wire/wire_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package rq2wire - -import "testing" - -func TestPipelineApplicabilityMatchesExecutedPayloadWork(t *testing.T) { - tests := []struct { - name, client, operation string - scanChunk, hash bool - }{ - {"browser document edit", ClientBrowserWASM, "document-edit-cid-binding-submit", true, true}, - {"browser map replace", ClientBrowserWASM, "map-replace", false, true}, - {"browser list append", ClientBrowserWASM, "list-append", false, true}, - {"native payload", ClientNative, "modify-small-file", true, true}, - {"native rename", ClientNative, "rename", false, false}, - {"native move", ClientNative, "move", false, false}, - {"native delete", ClientNative, "delete-directory-entry", false, false}, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - scanChunk, hash := pipelineApplicability(test.client, test.operation) - if scanChunk != test.scanChunk || hash != test.hash { - t.Fatalf("pipelineApplicability(%q, %q) = %t/%t, want %t/%t", test.client, test.operation, scanChunk, hash, test.scanChunk, test.hash) - } - }) - } -} diff --git a/internal/evaluation/rq2write/project.go b/internal/evaluation/rq2write/project.go deleted file mode 100644 index 4988a0d..0000000 --- a/internal/evaluation/rq2write/project.go +++ /dev/null @@ -1,166 +0,0 @@ -// Package rq2write projects the source fixture's file operations directly into -// typed Core deltas. Native filesystem and browser source preparation remain -// separately measured callers of this shared application projection. -package rq2write - -import ( - "bytes" - "context" - "fmt" - "math" - - "github.com/dewebprotocol/malt-client/internal/evaluation/authenticationgraph" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture" - unixfsmodel "github.com/dewebprotocol/malt-client/unixfs/model" - "github.com/dewebprotocol/malt-core/auth/coordinate" - "github.com/dewebprotocol/malt-core/derivation" - "github.com/dewebprotocol/malt-core/engine" - "github.com/dewebprotocol/malt-core/maltcid" - "github.com/dewebprotocol/malt-core/sdk/authentication" - cid "github.com/ipfs/go-cid" -) - -func Apply(ctx context.Context, edit *authenticationgraph.Edit, root cid.Cid, operation rq2fixture.Operation, payloads []cid.Cid) (cid.Cid, error) { - state, err := edit.State(root) - if err != nil { - return cid.Undef, err - } - if state.Descriptor.Layout != maltcid.Prefix || state.Descriptor.DerivationProfile != uint8(derivation.SHA256) { - return cid.Undef, fmt.Errorf("RQ2 file root requires Prefix AA1") - } - for _, payload := range payloads { - if !payload.Defined() || payload.Type() != cid.Raw { - return cid.Undef, fmt.Errorf("file operation requires raw payload CIDs") - } - } - if operation.Kind == rq2fixture.KindListAppend || operation.Kind == rq2fixture.KindListReplace { - edge, err := entryAt(state, operation.SourcePath) - if err != nil { - return cid.Undef, err - } - child, err := edit.State(edge.Target) - if err != nil { - return cid.Undef, err - } - if child.Descriptor.Layout != maltcid.Positional || child.Descriptor.DerivationProfile != uint8(derivation.Direct) || child.Descriptor.Profile != state.Descriptor.Profile || !child.PayloadCID.Defined() { - return cid.Undef, fmt.Errorf("file requires a measured Positional Root with the parent's VC profile") - } - if len(payloads) != 1 { - return cid.Undef, fmt.Errorf("chunk operation requires one payload") - } - geometry, err := unixfsmodel.ParseInlineChunkMetadata(child.PayloadCID, uint64(len(child.Entries))) - if err != nil { - return cid.Undef, err - } - var delta authentication.Delta - if operation.Kind == rq2fixture.KindListAppend { - if geometry.TotalSize%geometry.ChunkSize != 0 || operation.PayloadBytes != geometry.ChunkSize || geometry.TotalSize > math.MaxUint64-geometry.ChunkSize { - return cid.Undef, fmt.Errorf("append requires a full final chunk and one same-width new chunk") - } - count := uint64(len(child.Entries)) + 1 - total := geometry.TotalSize + geometry.ChunkSize - payload, err := unixfsmodel.InlineChunkMetadata(total, geometry.ChunkSize, count) - if err != nil { - return cid.Undef, err - } - delta = authentication.Delta{Changes: []engine.Change{{Label: coordinate.EncodeIndex(count - 1), After: payloads[0]}}, Count: &count, PayloadCID: &payload} - } else { - if operation.ListIndex == nil || *operation.ListIndex >= uint64(len(child.Entries)) { - return cid.Undef, fmt.Errorf("replacement index is outside measured file") - } - index := *operation.ListIndex - size := min(geometry.ChunkSize, geometry.TotalSize-index*geometry.ChunkSize) - if operation.PayloadBytes != size { - return cid.Undef, fmt.Errorf("replacement must preserve the authenticated chunk length") - } - var before cid.Cid - for _, entry := range child.Entries { - if bytes.Equal(entry.Label, coordinate.EncodeIndex(index)) { - before = entry.Target - break - } - } - if !before.Defined() { - return cid.Undef, fmt.Errorf("replacement index is absent") - } - delta.Changes = []engine.Change{{Label: coordinate.EncodeIndex(index), Before: before, After: payloads[0]}} - } - nextChild, err := edit.Apply(ctx, edge.Target, delta) - if err != nil { - return cid.Undef, err - } - // Select the caller's exact file edge; never rewrite every parent of an - // aliased immutable child or reject a valid copy-on-write graph. - return edit.Apply(ctx, root, authentication.Delta{Changes: []engine.Change{{Label: edge.Label, Before: edge.Target, After: nextChild}}}) - } - insert := func(path string, payload cid.Cid) (engine.Change, error) { - if path == "" { - return engine.Change{}, fmt.Errorf("insert path is empty") - } - for _, entry := range state.Entries { - if bytes.Equal(entry.Label, []byte(path)) { - return engine.Change{}, fmt.Errorf("destination %q already exists", path) - } - } - return engine.Change{Label: []byte(path), After: payload}, nil - } - var changes []engine.Change - switch operation.Kind { - case rq2fixture.KindBatchInsert: - if len(payloads) != len(operation.Batch) || len(payloads) == 0 { - return cid.Undef, fmt.Errorf("batch payload count differs from declared destinations") - } - for i, payload := range payloads { - change, err := insert(operation.Batch[i].Path, payload) - if err != nil { - return cid.Undef, err - } - changes = append(changes, change) - } - case rq2fixture.KindDirectInsert: - if len(payloads) != 1 { - return cid.Undef, fmt.Errorf("insert requires one payload") - } - change, err := insert(operation.DestinationPath, payloads[0]) - if err != nil { - return cid.Undef, err - } - changes = []engine.Change{change} - case rq2fixture.KindDirectReplace, rq2fixture.KindDocumentEdit, rq2fixture.KindDirectDelete, rq2fixture.KindDirectMove: - entry, err := entryAt(state, operation.SourcePath) - if err != nil { - return cid.Undef, err - } - if entry.Target.Type() != cid.Raw { - return cid.Undef, fmt.Errorf("source %q is not a direct file", operation.SourcePath) - } - change := engine.Change{Label: entry.Label, Before: entry.Target} - if operation.Kind == rq2fixture.KindDirectReplace || operation.Kind == rq2fixture.KindDocumentEdit { - if len(payloads) != 1 { - return cid.Undef, fmt.Errorf("replacement requires one payload") - } - change.After = payloads[0] - } else if len(payloads) != 0 { - return cid.Undef, fmt.Errorf("metadata operation cannot carry payloads") - } - changes = []engine.Change{change} - if operation.Kind == rq2fixture.KindDirectMove { - destination, err := insert(operation.DestinationPath, entry.Target) - if err != nil { - return cid.Undef, err - } - changes = append(changes, destination) - } - default: - return cid.Undef, fmt.Errorf("unsupported source operation %q", operation.Kind) - } - return edit.Apply(ctx, root, authentication.Delta{Changes: changes}) -} -func entryAt(state engine.State, path string) (engine.Entry, error) { - for _, entry := range state.Entries { - if bytes.Equal(entry.Label, []byte(path)) { - return entry, nil - } - } - return engine.Entry{}, fmt.Errorf("source %q is absent", path) -} diff --git a/internal/evaluation/rq2write/project_test.go b/internal/evaluation/rq2write/project_test.go deleted file mode 100644 index f5f51fa..0000000 --- a/internal/evaluation/rq2write/project_test.go +++ /dev/null @@ -1,148 +0,0 @@ -package rq2write_test - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "fmt" - "strings" - "testing" - - "github.com/dewebprotocol/malt-client/internal/evaluation/authenticationgraph" - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2e0" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2write" - "github.com/dewebprotocol/malt-client/transport" - "github.com/dewebprotocol/malt-core/auth/commitment/ipa" - "github.com/dewebprotocol/malt-core/auth/commitment/kzg" - "github.com/dewebprotocol/malt-core/engine" - cid "github.com/ipfs/go-cid" -) - -func TestCurrentProjectionAllFileOperationsAndSharedMeasuredChildren(t *testing.T) { - ctx := context.Background() - seed := sha256.Sum256([]byte("typed RQ2 fixture")) - zero := uint64(0) - source := &rq2fixture.SourceDefinition{SchemaVersion: rq2fixture.SourceSchemaVersion, FixtureID: "typed-projection", MutationSeedSHA256: hex.EncodeToString(seed[:]), DirectFiles: []rq2fixture.SourceDirectFile{}, Operations: []rq2fixture.Operation{ - {Name: "append", Kind: rq2fixture.KindListAppend, SourcePath: "large.bin", SourceCoordinate: "large.bin", PayloadBytes: 4}, - {Name: "replace-large-file-chunk", Kind: rq2fixture.KindListReplace, SourcePath: "large.bin", SourceCoordinate: "large.bin", PayloadBytes: 4, ListIndex: &zero}, - {Name: "batch-sync", Kind: rq2fixture.KindBatchInsert, Batch: []rq2fixture.BatchTarget{{Path: "batch/one", Coordinate: "batch/one", PayloadBytes: 4}, {Path: "batch/two", Coordinate: "batch/two", PayloadBytes: 4}}}, - {Name: "create-small-file", Kind: rq2fixture.KindDirectInsert, DestinationPath: "created", DestinationCoordinate: "created", PayloadBytes: 4}, - {Name: "insert-directory-entry", Kind: rq2fixture.KindDirectInsert, DestinationPath: "inserted", DestinationCoordinate: "inserted", PayloadBytes: 4}, - {Name: "modify-small-file", Kind: rq2fixture.KindDirectReplace, SourcePath: "modify", SourceCoordinate: "modify", PayloadBytes: 4}, - {Name: "delete-directory-entry", Kind: rq2fixture.KindDirectDelete, SourcePath: "delete", SourceCoordinate: "delete"}, - {Name: "rename", Kind: rq2fixture.KindDirectMove, SourcePath: "rename", SourceCoordinate: "rename", DestinationPath: "renamed", DestinationCoordinate: "renamed"}, - {Name: "move", Kind: rq2fixture.KindDirectMove, SourcePath: "move", SourceCoordinate: "move", DestinationPath: "directory/moved", DestinationCoordinate: "directory/moved"}, - {Name: "document-edit-cid-binding-submit", Kind: rq2fixture.KindDocumentEdit, SourcePath: "document", SourceCoordinate: "document", PayloadBytes: 4}, - }} - for _, path := range []string{"modify", "delete", "rename", "move", "document"} { - source.DirectFiles = append(source.DirectFiles, rq2fixture.SourceDirectFile{Path: path, Coordinate: path, Bytes: []byte("old " + path)}) - } - for _, path := range []string{"large.bin", "alias.bin"} { - source.ListFiles = append(source.ListFiles, rq2fixture.SourceListFile{Path: path, Coordinate: path, ChunkSize: 4, TotalSize: 8, Chunks: []rq2fixture.SourceListChunk{{Index: 0, Bytes: []byte("one!")}, {Index: 1, Bytes: []byte("two!")}}}) - } - fixture, err := rq2e0.BuildFixture(ctx, source) - if err != nil { - t.Fatal(err) - } - for _, backend := range []string{"kzg", "ipa"} { - t.Run(backend, func(t *testing.T) { - gateway, root, err := rq2e0.NewConformanceGateway(fixture, backend, strings.Repeat("a", 64)) - if err != nil { - t.Fatal(err) - } - defer gateway.Close() - remote, err := gatewaytransport.New(gatewaytransport.Options{BaseURL: gateway.URL(), InstanceToken: strings.Repeat("a", 64)}) - if err != nil { - t.Fatal(err) - } - payloads, err := transport.New(transport.Options{BaseURL: gateway.URL(), HTTPClient: remote.InstanceHTTPClient()}) - if err != nil { - t.Fatal(err) - } - var scheme engine.Profile - if backend == "kzg" { - scheme, err = kzg.NewScheme() - } else { - scheme, err = ipa.NewScheme() - } - if err != nil { - t.Fatal(err) - } - registry := engine.NewRegistry() - if err := registry.Register(scheme); err != nil { - t.Fatal(err) - } - session, err := authenticationgraph.New(remote, engine.New(registry)) - if err != nil { - t.Fatal(err) - } - load, err := session.Load(ctx, root) - if err != nil { - t.Fatal(err) - } - if load.Objects != 2 { - t.Fatal("shared child imported twice") - } - initial, err := session.Snapshot() - if err != nil { - t.Fatal(err) - } - if err := fixture.ValidateInitialGraph(initial, backend); err != nil { - t.Fatal(err) - } - currentSource := fixture.InitialSource() - for i, operation := range source.Operations { - nextSource, bodies, err := fixture.ApplySourceOperation(currentSource, operation, uint64(i)) - if err != nil { - t.Fatal(err) - } - targets := make([]cid.Cid, len(bodies)) - if len(bodies) > 0 { - blocks := make([]transport.Block, len(bodies)) - for j, body := range bodies { - blocks[j] = transport.Block{Codec: cid.Raw, Data: body} - } - upload, err := payloads.PutBatchMeasured(ctx, blocks) - if err != nil { - t.Fatal(err) - } - for j, result := range upload.Results { - targets[j] = result.CID - } - } - edit, err := session.Begin() - if err != nil { - t.Fatal(err) - } - next, err := rq2write.Apply(ctx, edit, root, operation, targets) - if err != nil { - t.Fatalf("%s: %v", operation.Name, err) - } - preview, err := edit.Preview(ctx, next) - if err != nil { - t.Fatal(err) - } - if err := fixture.ValidateGraphAgainstSource(preview, backend, nextSource); err != nil { - t.Fatalf("%s full oracle: %v", operation.Name, err) - } - result, err := session.Submit(ctx, fmt.Sprintf("operation-%d", i), edit, next) - if err != nil { - t.Fatal(err) - } - if result.Idempotent || result.Submission.WriteAccounting.Available { - t.Fatal("correctness oracle claimed production accounting or returned replay") - } - currentSource = nextSource - root = next - } - if err := session.Audit(ctx); err != nil { - t.Fatal(err) - } - if got := gateway.Operations(); got != uint64(len(source.Operations)) { - t.Fatalf("operations = %d", got) - } - }) - } -} diff --git a/internal/evaluation/rq3baseline/archive_index.go b/internal/evaluation/rq3baseline/archive_index.go deleted file mode 100644 index 3b9fcbd..0000000 --- a/internal/evaluation/rq3baseline/archive_index.go +++ /dev/null @@ -1,44 +0,0 @@ -package rq3baseline - -import ( - "sync" - - "github.com/syndtr/goleveldb/leveldb/storage" -) - -// Meter the public LevelDB storage writer, including WAL/table/manifest writes -// and compaction. CURRENT and diagnostic logging bypass this interface; their -// retained sizes are inventoried, and write coverage is explicitly incomplete. -type meteredIndexStorage struct { - storage.Storage - mu sync.Mutex - bytes, calls int64 -} - -func (s *meteredIndexStorage) Create(fd storage.FileDesc) (storage.Writer, error) { - w, err := s.Storage.Create(fd) - if err != nil { - return nil, err - } - return &meteredIndexWriter{Writer: w, owner: s}, nil -} - -func (s *meteredIndexStorage) snapshot() (int64, int64) { - s.mu.Lock() - defer s.mu.Unlock() - return s.bytes, s.calls -} - -type meteredIndexWriter struct { - storage.Writer - owner *meteredIndexStorage -} - -func (w *meteredIndexWriter) Write(p []byte) (int, error) { - n, err := w.Writer.Write(p) - w.owner.mu.Lock() - w.owner.bytes += int64(n) - w.owner.calls++ - w.owner.mu.Unlock() - return n, err -} diff --git a/internal/evaluation/rq3baseline/archive_store.go b/internal/evaluation/rq3baseline/archive_store.go deleted file mode 100644 index 842fb58..0000000 --- a/internal/evaluation/rq3baseline/archive_store.go +++ /dev/null @@ -1,461 +0,0 @@ -package rq3baseline - -import ( - "bufio" - "bytes" - "container/list" - "encoding/binary" - "errors" - "fmt" - "io" - "os" - "path/filepath" - - cid "github.com/ipfs/go-cid" - car "github.com/ipld/go-car/v2" - carstorage "github.com/ipld/go-car/v2/storage" - "github.com/syndtr/goleveldb/leveldb" - "github.com/syndtr/goleveldb/leveldb/filter" - "github.com/syndtr/goleveldb/leveldb/opt" - "github.com/syndtr/goleveldb/leveldb/storage" -) - -const ReplayStorageBackend = "segmented-carv1-leveldb/v1" - -const ( - archiveSegmentBytes = 1 << 30 - archiveCacheBytes = 32 << 20 - archiveWriteBufferBytes = 8 << 20 - archiveReadHandles = 8 - archiveIndexHandles = 32 - archiveMaxSectionBytes = MaximumJSONLRecordBytes -) - -// ReplayStorage is a terminal, CAS-only observation after independent CAR/index -// reconciliation and index close. File sizes are logical lengths, not allocated -// sectors. Storage.Writer counters exclude LevelDB's internal CURRENT/LOG writes. -// Source checkout staging and evaluator output files are outside this scope. -type ReplayStorage struct { - Schema string `json:"schema_version"` - Backend string `json:"backend"` - SegmentLimit int64 `json:"segment_limit_bytes,string"` - IndexCache int64 `json:"index_block_cache_bytes,string"` - IndexBuffer int64 `json:"index_write_buffer_bytes,string"` - IndexHandles int `json:"index_open_files_cache"` - ReadHandles int `json:"car_read_handles"` - Objects int64 `json:"cas_objects,string"` - BodyBytes int64 `json:"cas_body_bytes,string"` - CARFiles int64 `json:"car_files,string"` - CARBytes int64 `json:"car_file_bytes,string"` - FramingBytes int64 `json:"car_framing_bytes,string"` - CARWriteBytes int64 `json:"car_write_bytes,string"` - IndexFiles int64 `json:"index_files,string"` - IndexBytes int64 `json:"index_file_bytes,string"` - IndexWriterBytes int64 `json:"index_storage_writer_bytes,string"` - IndexWriterCalls int64 `json:"index_storage_writer_calls,string"` - IndexWriterComplete bool `json:"index_writer_coverage_complete"` - IndexWriterExclusions []string `json:"index_writer_exclusions"` - Entries int64 `json:"cas_filesystem_entries,string"` - Reconciled bool `json:"car_index_reconciled"` -} - -type archiveLocation struct { - segment uint32 - offset, length uint64 -} - -func (p archiveLocation) encode() []byte { - b := make([]byte, 20) - binary.LittleEndian.PutUint32(b, p.segment) - binary.LittleEndian.PutUint64(b[4:], p.offset) - binary.LittleEndian.PutUint64(b[12:], p.length) - return b -} - -func decodeArchiveLocation(b []byte) (archiveLocation, error) { - if len(b) != 20 { - return archiveLocation{}, fmt.Errorf("invalid CAR index location") - } - p := archiveLocation{binary.LittleEndian.Uint32(b), binary.LittleEndian.Uint64(b[4:]), binary.LittleEndian.Uint64(b[12:])} - if p.segment == 0 || p.offset > uint64(^uint64(0)>>1) || p.length == 0 || p.length > archiveMaxSectionBytes+10 || p.offset+p.length < p.offset { - return archiveLocation{}, fmt.Errorf("CAR index location exceeds bounds") - } - return p, nil -} - -type archiveReader struct { - segment uint32 - file *os.File -} - -// archiveCAS is owned by accountingStore.mu. Only LevelDB's metered storage is -// concurrently accessed. No in-memory per-object index is retained here. -type archiveCAS struct { - root string - index *leveldb.DB - indexStorage *meteredIndexStorage - header []byte - segmentLimit int64 - segments uint32 - active *os.File - activeBytes int64 - carWritten int64 - readers *list.List - failed error - sealed bool - handlesClosed bool - closeErr error -} - -func newArchiveCAS(root string, segmentLimit int64) (*archiveCAS, error) { - // Use the standard writer only for an empty rootless CARv1 header. Blocks - // are appended below; go-car's growing in-memory insertion index is unused. - var header bytes.Buffer - w, err := carstorage.NewWritable(&header, []cid.Cid{}, car.WriteAsCarV1(true)) - if err != nil { - return nil, err - } - if err := w.Finalize(); err != nil { - return nil, err - } - if segmentLimit <= int64(header.Len()) { - return nil, fmt.Errorf("CAR segment limit too small") - } - if err := os.Mkdir(filepath.Join(root, "data"), 0o700); err != nil { - return nil, err - } - fs, err := storage.OpenFile(filepath.Join(root, "index"), false) - if err != nil { - return nil, err - } - meter := &meteredIndexStorage{Storage: fs} - db, err := leveldb.Open(meter, &opt.Options{ - ErrorIfExist: true, Compression: opt.NoCompression, Strict: opt.StrictAll, - BlockCacheCapacity: archiveCacheBytes, WriteBuffer: archiveWriteBufferBytes, - OpenFilesCacheCapacity: archiveIndexHandles, Filter: filter.NewBloomFilter(10), - }) - if err != nil { - return nil, errors.Join(err, fs.Close()) - } - return &archiveCAS{root: root, index: db, indexStorage: meter, header: append([]byte(nil), header.Bytes()...), segmentLimit: segmentLimit, readers: list.New()}, nil -} - -func (s *archiveCAS) path(segment uint32) string { - return filepath.Join(s.root, "data", fmt.Sprintf("%08d.car", segment)) -} - -func (s *archiveCAS) ready() error { - if s.failed != nil { - return s.failed - } - if s.sealed || s.index == nil { - return fmt.Errorf("replay CAR store is sealed or closed") - } - return nil -} - -func (s *archiveCAS) get(key cid.Cid) ([]byte, error) { - if err := s.ready(); err != nil { - return nil, err - } - value, err := s.index.Get(key.Bytes(), nil) - if errors.Is(err, leveldb.ErrNotFound) { - return nil, os.ErrNotExist - } - if err != nil { - return nil, err - } - p, err := decodeArchiveLocation(value) - if err != nil { - return nil, err - } - if p.segment > s.segments || p.offset < uint64(len(s.header)) { - return nil, fmt.Errorf("unknown CAR segment or header offset") - } - f, err := s.reader(p.segment) - if err != nil { - return nil, fmt.Errorf("indexed CAR segment is unavailable: %v", err) - } - info, err := f.Stat() - if err != nil { - return nil, err - } - if !info.Mode().IsRegular() || p.offset > uint64(info.Size()) || p.length > uint64(info.Size())-p.offset { - return nil, fmt.Errorf("CAR index points outside a regular segment") - } - frame := make([]byte, int(p.length)) - if _, err := f.ReadAt(frame, int64(p.offset)); err != nil { - return nil, err - } - length, n := binary.Uvarint(frame) - if n <= 0 || length != uint64(len(frame)-n) || length > archiveMaxSectionBytes { - return nil, fmt.Errorf("invalid CAR block framing") - } - if !bytes.Equal(binary.AppendUvarint(nil, length), frame[:n]) { - return nil, fmt.Errorf("noncanonical CAR section length") - } - cidBytes, actual, err := cid.CidFromBytes(frame[n:]) - if err != nil || !actual.Equals(key) { - return nil, fmt.Errorf("CAR index does not bind complete CID %s", key) - } - data := frame[n+cidBytes:] - computed, err := key.Prefix().Sum(data) - if err != nil || !computed.Equals(key) { - return nil, fmt.Errorf("CAR block does not match CID %s", key) - } - return data, nil -} - -func (s *archiveCAS) reader(segment uint32) (*os.File, error) { - if segment == s.segments && s.active != nil { - return s.active, nil - } - for e := s.readers.Front(); e != nil; e = e.Next() { - r := e.Value.(archiveReader) - if r.segment == segment { - s.readers.MoveToFront(e) - return r.file, nil - } - } - if s.readers.Len() == archiveReadHandles { - e := s.readers.Back() - s.readers.Remove(e) - if err := e.Value.(archiveReader).file.Close(); err != nil { - s.failed = err - return nil, err - } - } - f, err := os.Open(s.path(segment)) - if err != nil { - return nil, err - } - s.readers.PushFront(archiveReader{segment, f}) - return f, nil -} - -func (s *archiveCAS) appendNew(key cid.Cid, data []byte) (err error) { - if err := s.ready(); err != nil { - return err - } - defer func() { - if err != nil { - s.failed = err - } - }() - section := uint64(key.ByteLen() + len(data)) - if section > archiveMaxSectionBytes { - return fmt.Errorf("CAR section exceeds replay record limit") - } - prefix := append(binary.AppendUvarint(nil, section), key.Bytes()...) - frameBytes := int64(len(prefix) + len(data)) - if frameBytes+int64(len(s.header)) > s.segmentLimit { - return fmt.Errorf("CAR block exceeds segment limit") - } - if s.active == nil || s.activeBytes+frameBytes > s.segmentLimit { - if s.active != nil { - f := s.active - s.active = nil - if err := f.Close(); err != nil { - return err - } - } - if s.segments == ^uint32(0) { - return fmt.Errorf("too many CAR segments") - } - s.segments++ - s.active, err = os.OpenFile(s.path(s.segments), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o600) - if err != nil { - return err - } - s.activeBytes = 0 - if err := s.write(s.header); err != nil { - return err - } - } - p := archiveLocation{s.segments, uint64(s.activeBytes), uint64(frameBytes)} - if err := s.write(prefix); err != nil { - return err - } - if err := s.write(data); err != nil { - return err - } - // Publication follows the complete frame. Any failure poisons this run; - // partially written data are never accepted as a resumable measurement. - return s.index.Put(key.Bytes(), p.encode(), &opt.WriteOptions{Sync: false}) -} - -func (s *archiveCAS) write(data []byte) error { - n, err := s.active.Write(data) - s.activeBytes += int64(n) - s.carWritten += int64(n) - if err == nil && n != len(data) { - err = io.ErrShortWrite - } - return err -} - -type countedReader struct { - io.Reader - n uint64 -} - -func (r *countedReader) Read(p []byte) (int, error) { - n, err := r.Reader.Read(p) - r.n += uint64(n) - return n, err -} - -// inventory scans every physical CAR frame with go-car's CID validation, then -// binds its exact location to the disk index. Duplicate frames, orphan frames, -// missing/extra index entries and truncated sections fail reconciliation. -func (s *archiveCAS) inventory() (report ReplayStorage, err error) { - if err := s.ready(); err != nil { - return report, err - } - entries, err := os.ReadDir(filepath.Join(s.root, "data")) - if err != nil { - return report, err - } - if len(entries) != int(s.segments) { - return report, fmt.Errorf("unexpected CAR segment set") - } - for segment := uint32(1); segment <= s.segments; segment++ { - if err := s.scanSegment(segment, &report); err != nil { - return report, err - } - } - iter := s.index.NewIterator(nil, &opt.ReadOptions{DontFillCache: true, Strict: opt.StrictAll}) - var indexed int64 - for iter.Next() { - indexed++ - } - err = iter.Error() - iter.Release() - if err != nil { - return report, err - } - if indexed != report.Objects { - return report, fmt.Errorf("CAR/index object count mismatch") - } - if report.CARBytes != s.carWritten { - return report, fmt.Errorf("CAR retained bytes differ from completed writes") - } - report.Schema, report.Backend = "malt-replay-cas-storage/v1", ReplayStorageBackend - report.SegmentLimit, report.IndexCache, report.IndexBuffer = s.segmentLimit, archiveCacheBytes, archiveWriteBufferBytes - report.ReadHandles, report.IndexHandles = archiveReadHandles, archiveIndexHandles - report.FramingBytes = report.CARBytes - report.BodyBytes - report.CARWriteBytes = s.carWritten - report.Reconciled = true - return report, nil -} - -func (s *archiveCAS) scanSegment(segment uint32, report *ReplayStorage) (returnErr error) { - f, err := os.Open(s.path(segment)) - if err != nil { - return err - } - defer func() { returnErr = errors.Join(returnErr, f.Close()) }() - info, err := f.Stat() - if err != nil { - return err - } - if !info.Mode().IsRegular() || info.Size() > s.segmentLimit { - return fmt.Errorf("invalid CAR segment") - } - reader := &countedReader{Reader: bufio.NewReaderSize(f, 256<<10)} - blocks, err := car.NewBlockReader(reader, car.MaxAllowedSectionSize(archiveMaxSectionBytes)) - if err != nil { - return err - } - if blocks.Version != 1 || len(blocks.Roots) != 0 || reader.n != uint64(len(s.header)) { - return fmt.Errorf("invalid replay CAR header") - } - for { - offset := reader.n - block, err := blocks.Next() - if errors.Is(err, io.EOF) { - if reader.n != offset || reader.n != uint64(info.Size()) { - return fmt.Errorf("truncated CAR frame") - } - break - } - if err != nil { - return err - } - value, err := s.index.Get(block.Cid().Bytes(), &opt.ReadOptions{DontFillCache: true}) - if err != nil { - return fmt.Errorf("CAR frame lacks index entry: %w", err) - } - want := archiveLocation{segment, offset, reader.n - offset} - if !bytes.Equal(value, want.encode()) { - return fmt.Errorf("CAR frame/index location mismatch") - } - report.Objects++ - report.BodyBytes += int64(len(block.RawData())) - } - report.CARFiles++ - report.CARBytes += info.Size() - return nil -} - -func (s *archiveCAS) finish() (ReplayStorage, error) { - report, err := s.inventory() - if err != nil { - return report, err - } - if err := s.closeHandles(); err != nil { - return report, err - } - report.IndexWriterBytes, report.IndexWriterCalls = s.indexStorage.snapshot() - report.IndexWriterExclusions = []string{"LevelDB CURRENT metadata", "LevelDB diagnostic LOG"} - err = filepath.WalkDir(s.root, func(path string, entry os.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - report.Entries++ - if entry.IsDir() { - return nil - } - info, err := entry.Info() - if err != nil { - return err - } - if !info.Mode().IsRegular() { - return fmt.Errorf("non-regular replay CAS entry") - } - if filepath.Dir(path) == filepath.Join(s.root, "index") { - report.IndexFiles++ - report.IndexBytes += info.Size() - } - return nil - }) - return report, err -} - -func (s *archiveCAS) closeHandles() error { - if s.handlesClosed { - return s.closeErr - } - s.handlesClosed = true - s.sealed = true - var errs []error - if s.active != nil { - f := s.active - s.active = nil - errs = append(errs, f.Close()) - } - for e := s.readers.Front(); e != nil; e = e.Next() { - errs = append(errs, e.Value.(archiveReader).file.Close()) - } - s.readers.Init() - if s.index != nil { - db := s.index - s.index = nil - errs = append(errs, db.Close()) - } - if s.indexStorage != nil { - errs = append(errs, s.indexStorage.Close()) - } - s.closeErr = errors.Join(errs...) - return s.closeErr -} diff --git a/internal/evaluation/rq3baseline/archive_store_test.go b/internal/evaluation/rq3baseline/archive_store_test.go deleted file mode 100644 index d69c5c6..0000000 --- a/internal/evaluation/rq3baseline/archive_store_test.go +++ /dev/null @@ -1,324 +0,0 @@ -package rq3baseline - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "os" - "path/filepath" - "reflect" - "testing" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - merkledag "github.com/ipfs/boxo/ipld/merkledag" - unixfs "github.com/ipfs/boxo/ipld/unixfs" - cid "github.com/ipfs/go-cid" -) - -func testArchive(t *testing.T, segmentBytes int64) *archiveCAS { - t.Helper() - s, err := newArchiveCAS(t.TempDir(), segmentBytes) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if err := s.closeHandles(); err != nil { - t.Error(err) - } - }) - return s -} - -func archiveKey(t *testing.T, data []byte, codec uint64) cid.Cid { - t.Helper() - key, err := clientcas.CIDForBlock(clientcas.Block{Data: data, Codec: codec}) - if err != nil { - t.Fatal(err) - } - return key -} - -func TestReplayArchiveMatchesFileStoreAcrossAllProfiles(t *testing.T) { - for _, profile := range []string{"merkledag-nested", "hamt-nested", "hamt-flat"} { - t.Run(profile, func(t *testing.T) { - system, directory, fanout := SystemHAMTUnixFS, "hamt", 256 - if profile == "merkledag-nested" { - system, directory, fanout = SystemMerkleDAGUnixFS, "basic", 0 - } - spec := baseSpec(system, directory, fanout, []FrozenFile{frozenFile("a/b", []byte("initial")), frozenFile("stable", []byte("unchanged"))}) - packed := func() *accountingStore { - s := newAccountingStore() - if s.initErr == nil { - s.archive, s.initErr = newArchiveCAS(s.root, 16<<10) - } - return s - } - var sessions []*StreamSession - var previous *CommitRecord - var expectedObjects, expectedBytes int64 - compare := func(a, b CommitRecord) { - t.Helper() - a.ClientPhases, b.ClientPhases = ClientPhases{}, ClientPhases{} - if !reflect.DeepEqual(a, b) { - t.Fatalf("logical record differs at %s", a.CommitID) - } - } - for _, factory := range []func() *accountingStore{newAccountingStore, packed} { - s, initial, err := startStreamWithStore(t.Context(), spec, profile == "hamt-flat", true, factory) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if err := s.Close(); err != nil { - t.Error(err) - } - }) - sessions = append(sessions, s) - if previous != nil { - compare(*previous, initial) - } else { - previous = &initial - } - } - expectedObjects += int64(previous.CAS.Total.NewlyPersistedObjects) - expectedBytes += previous.CAS.Total.NewlyPersistedBytes - body := []byte("initial") - for i := 0; i < 80; i++ { - next := []byte(fmt.Sprintf("version-%d-%s", i%7, bytes.Repeat([]byte("x"), i%11))) - if i%9 == 0 { - next = body - } // no-op and cross-commit reuse - commit := Commit{CommitID: fmt.Sprintf("archive-update-%d", i), Mutations: []Mutation{replaceMutation("a/b", body, next)}} - var records []CommitRecord - for _, session := range sessions { - got, err := session.ApplyChunkWithReadback(t.Context(), []Commit{commit}) - if err != nil { - t.Fatal(err) - } - records = append(records, got[0]) - if err := session.VerifyAll(t.Context()); err != nil { - t.Fatal(err) - } - } - compare(records[0], records[1]) - expectedObjects += int64(records[0].CAS.Total.NewlyPersistedObjects) - expectedBytes += records[0].CAS.Total.NewlyPersistedBytes - body = next - } - for _, session := range sessions { - objects, size, err := session.RetainedCAS() - if err != nil || objects != expectedObjects || size != expectedBytes { - t.Fatalf("inventory = %d/%d, want %d/%d: %v", objects, size, expectedObjects, expectedBytes, err) - } - } - report, err := sessions[1].FinishReplayStorage() - if err != nil { - t.Fatal(err) - } - if !report.Reconciled || report.CARWriteBytes != report.CARBytes || report.CARBytes-report.FramingBytes != expectedBytes || report.IndexFiles == 0 || report.IndexWriterBytes == 0 || report.IndexWriterComplete { - t.Fatalf("invalid backend report: %+v", report) - } - if _, err := sessions[1].store.Get(t.Context(), archiveKey(t, body, cid.Raw)); err == nil { - t.Fatal("sealed store accepted reads") - } - }) - } -} - -func TestReplayArchiveUsesWholeCIDAndPreservesDedup(t *testing.T) { - s := newReplayAccountingStore() - t.Cleanup(func() { - if err := s.close(); err != nil { - t.Error(err) - } - }) - data, err := merkledag.NodeWithData(unixfs.FolderPBData()).EncodeProtobuf(false) - if err != nil { - t.Fatal(err) - } - s.beginPhase() - for _, codec := range []uint64{cid.Raw, cid.DagProtobuf, cid.Raw, cid.DagProtobuf} { - key, err := s.PutWithCodec(t.Context(), data, codec) - if err != nil { - t.Fatal(err) - } - got, err := s.Get(t.Context(), key) - if err != nil || !bytes.Equal(data, got) { - t.Fatalf("whole CID lookup failed: %v", err) - } - } - first, _, _ := s.finishPhase() - if first.Total.NewlyPersistedObjects != 2 || first.Total.DuplicateObjects != 2 { - t.Fatalf("dedup = %+v", first.Total) - } - s.beginPhase() - if _, err := s.PutWithCodec(t.Context(), data, cid.Raw); err != nil { - t.Fatal(err) - } - second, _, _ := s.finishPhase() - if second.Total.AlreadyPresentObjects != 1 || second.Total.NewlyPersistedObjects != 0 { - t.Fatal("cross-phase dedup changed") - } - report, err := s.archive.inventory() - if err != nil || report.Objects != 2 { - t.Fatalf("whole CID inventory: %+v, %v", report, err) - } -} - -func TestReplayArchiveManyObjectsUseFewFiles(t *testing.T) { - s := testArchive(t, 64<<10) - const count = 20000 - var total int64 - for i := 0; i < count; i++ { - data := binary.LittleEndian.AppendUint64(nil, uint64(i)) - if err := s.appendNew(archiveKey(t, data, cid.Raw), data); err != nil { - t.Fatal(err) - } - total += int64(len(data)) - } - // Reverse-order lookups span many sealed shards and exercise eviction. - for i := count - 1; i >= 0; i -= 101 { - data := binary.LittleEndian.AppendUint64(nil, uint64(i)) - got, err := s.get(archiveKey(t, data, cid.Raw)) - if err != nil || !bytes.Equal(got, data) { - t.Fatalf("read %d: %v", i, err) - } - if s.readers.Len() > archiveReadHandles { - t.Fatal("unbounded reader handles") - } - } - report, err := s.finish() - if err != nil { - t.Fatal(err) - } - if report.Objects != count || report.BodyBytes != total || report.CARFiles < 2 || report.Entries >= 100 { - t.Fatalf("inode reduction failed: %+v", report) - } - t.Logf("%d objects, %d CAR segments, %d CAS filesystem entries, %d body bytes, %d CAR bytes, %d index bytes", count, report.CARFiles, report.Entries, report.BodyBytes, report.CARBytes, report.IndexBytes) -} - -func TestReplayArchiveRejectsCorruptOrIncompleteStorage(t *testing.T) { - for _, scenario := range []string{"body", "index-offset", "missing-index", "extra-index", "truncated-frame", "truncated-varint", "duplicate-frame", "missing-segment"} { - t.Run(scenario, func(t *testing.T) { - s := testArchive(t, 4096) - data := []byte("checked payload") - key := archiveKey(t, data, cid.Raw) - if err := s.appendNew(key, data); err != nil { - t.Fatal(err) - } - location, err := s.index.Get(key.Bytes(), nil) - if err != nil { - t.Fatal(err) - } - p, err := decodeArchiveLocation(location) - if err != nil { - t.Fatal(err) - } - switch scenario { - case "body": - _, err = s.active.WriteAt([]byte("!"), int64(p.offset+p.length-1)) - case "index-offset": - p.offset++ - err = s.index.Put(key.Bytes(), p.encode(), nil) - case "missing-index": - err = s.index.Delete(key.Bytes(), nil) - case "extra-index": - err = s.index.Put(archiveKey(t, []byte("phantom"), cid.Raw).Bytes(), location, nil) - case "truncated-frame": - err = s.active.Truncate(s.activeBytes - 1) - case "truncated-varint": - err = s.write([]byte{0x80}) - case "duplicate-frame": - err = s.appendNew(key, data) - case "missing-segment": - f := s.active - s.active = nil - err = f.Close() - if err == nil { - err = os.Remove(s.path(1)) - } - } - if err != nil { - t.Fatal(err) - } - if scenario == "body" || scenario == "index-offset" || scenario == "missing-segment" || scenario == "truncated-frame" { - if _, err := s.get(key); err == nil || os.IsNotExist(err) { - t.Fatalf("corruption treated as valid/missing block: %v", err) - } - } - if _, err := s.inventory(); err == nil { - t.Fatal("damaged CAR/index accepted") - } - }) - } -} - -func TestReplayArchiveFailurePoisonsAppend(t *testing.T) { - s := testArchive(t, 4096) - data := []byte("first") - if err := s.appendNew(archiveKey(t, data, cid.Raw), data); err != nil { - t.Fatal(err) - } - // Read-only replacement deterministically injects a failed filesystem write. - if err := s.active.Close(); err != nil { - t.Fatal(err) - } - var err error - s.active, err = os.Open(s.path(1)) - if err != nil { - t.Fatal(err) - } - for _, text := range []string{"second", "third"} { - body := []byte(text) - if err := s.appendNew(archiveKey(t, body, cid.Raw), body); err == nil { - t.Fatal("failed archive continued accepting writes") - } - } - if _, err := s.inventory(); err == nil { - t.Fatal("failed run became a complete inventory") - } - missing := archiveKey(t, []byte("absent"), cid.Raw) - if _, err := s.get(missing); errors.Is(err, os.ErrNotExist) { - t.Fatal("poisoned store treated as normal absence") - } -} - -func TestReplayArchiveIndexFailureRetainsUnpublishedFrame(t *testing.T) { - s := testArchive(t, 4096) - data := []byte("first") - if err := s.appendNew(archiveKey(t, data, cid.Raw), data); err != nil { - t.Fatal(err) - } - before := s.carWritten - if err := s.index.SetReadOnly(); err != nil { - t.Fatal(err) - } - data = []byte("index publication fails after CAR append") - if err := s.appendNew(archiveKey(t, data, cid.Raw), data); err == nil { - t.Fatal("read-only index accepted append") - } - if s.carWritten <= before { - t.Fatal("partial execution's actual CAR write was erased") - } - if _, err := s.inventory(); err == nil { - t.Fatal("unpublished frame accepted as completed replay") - } -} - -func TestReplayArchiveCreationRejectsExistingIndex(t *testing.T) { - directory := t.TempDir() - s, err := newArchiveCAS(directory, 4096) - if err != nil { - t.Fatal(err) - } - if err := s.closeHandles(); err != nil { - t.Fatal(err) - } - if _, err := newArchiveCAS(directory, 4096); err == nil { - t.Fatal("existing run reused") - } - if _, err := os.Stat(filepath.Join(directory, "index")); err != nil { - t.Fatal(err) - } -} diff --git a/internal/evaluation/rq3baseline/payload_store.go b/internal/evaluation/rq3baseline/payload_store.go deleted file mode 100644 index bc306fe..0000000 --- a/internal/evaluation/rq3baseline/payload_store.go +++ /dev/null @@ -1,150 +0,0 @@ -package rq3baseline - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "os" - "path/filepath" -) - -// payloadStore keeps live checkout content out of the worker heap. Files are -// immutable and addressed by the already-validated payload SHA-256. -type payloadStore struct { - directory string - refs map[string]uint64 -} - -func newPayloadStore() (*payloadStore, error) { - directory, err := os.MkdirTemp("", "malt-rq3-payloads-") - if err != nil { - return nil, err - } - return &payloadStore{directory: directory, refs: make(map[string]uint64)}, nil -} - -func (s *payloadStore) put(digest string, data []byte) error { - if s == nil || s.directory == "" { - return fmt.Errorf("RQ3 payload store is closed") - } - actual := sha256.Sum256(data) - if hex.EncodeToString(actual[:]) != digest { - return fmt.Errorf("RQ3 payload store object %q has a mismatched digest", digest) - } - if s.refs[digest] > 0 { - s.refs[digest]++ - return nil - } - path := filepath.Join(s.directory, digest) - file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) - if os.IsExist(err) { - existing, readErr := os.ReadFile(path) - if readErr != nil { - return readErr - } - if !bytes.Equal(existing, data) { - return fmt.Errorf("RQ3 payload store object %q differs from its existing content", digest) - } - s.refs[digest]++ - return nil - } - if err != nil { - return err - } - if _, err := file.Write(data); err != nil { - return cleanupFailedPayloadFile(path, err, file.Close(), os.Remove) - } - if err := file.Close(); err != nil { - return cleanupFailedPayloadFile(path, err, nil, os.Remove) - } - s.refs[digest]++ - return nil -} - -func cleanupFailedPayloadFile(path string, primary, closeErr error, remove func(string) error) error { - removeErr := remove(path) - if os.IsNotExist(removeErr) { - removeErr = nil - } - return errors.Join(primary, closeErr, removeErr) -} - -func (s *payloadStore) read(file logicalFile) ([]byte, error) { - if file.data != nil { - return file.data, nil - } - if s == nil || s.directory == "" { - return nil, fmt.Errorf("RQ3 payload store is closed") - } - data, err := os.ReadFile(filepath.Join(s.directory, file.hash)) - if err != nil { - return nil, err - } - digest := sha256.Sum256(data) - if len(data) != file.size || hex.EncodeToString(digest[:]) != file.hash { - return nil, fmt.Errorf("RQ3 payload store object %q failed validation", file.hash) - } - return data, nil -} - -// reconcile retains exactly the immutable payloads referenced by the supplied -// live semantic and execution states. Replaced and deleted content is removed -// between chunks so disk use follows the live checkout rather than history. -func (s *payloadStore) reconcile(states ...map[string]logicalFile) error { - if s == nil || s.directory == "" { - return fmt.Errorf("RQ3 payload store is closed") - } - live := make(map[string]uint64) - for _, state := range states { - for _, file := range state { - if file.hash == "" { - return fmt.Errorf("RQ3 live payload has an empty digest") - } - live[file.hash]++ - } - } - var reconcileErr error - for digest := range s.refs { - if live[digest] != 0 { - continue - } - if err := os.Remove(filepath.Join(s.directory, digest)); err != nil && !os.IsNotExist(err) { - reconcileErr = errors.Join(reconcileErr, fmt.Errorf("remove unreferenced RQ3 payload %q: %w", digest, err)) - continue - } - delete(s.refs, digest) - } - for digest, count := range live { - if _, err := os.Stat(filepath.Join(s.directory, digest)); err != nil { - reconcileErr = errors.Join(reconcileErr, fmt.Errorf("stat live RQ3 payload %q: %w", digest, err)) - continue - } - s.refs[digest] = count - } - return reconcileErr -} - -func (s *payloadStore) close() error { - if s == nil || s.directory == "" { - return nil - } - directory := s.directory - if err := os.RemoveAll(directory); err != nil { - return err - } - s.directory = "" - s.refs = nil - return nil -} - -func retainLogicalFile(data []byte, mode uint32, digest string, store *payloadStore) (logicalFile, error) { - if store == nil { - return logicalFile{data: cloneBytes(data), mode: mode, hash: digest, size: len(data)}, nil - } - if err := store.put(digest, data); err != nil { - return logicalFile{}, err - } - return logicalFile{mode: mode, hash: digest, size: len(data)}, nil -} diff --git a/internal/evaluation/rq3baseline/readback.go b/internal/evaluation/rq3baseline/readback.go deleted file mode 100644 index ba45279..0000000 --- a/internal/evaluation/rq3baseline/readback.go +++ /dev/null @@ -1,267 +0,0 @@ -package rq3baseline - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "sort" - "strings" - - merkledagimport "github.com/dewebprotocol/malt-client/merkledag/importer" - merkledag "github.com/ipfs/boxo/ipld/merkledag" - unixfs "github.com/ipfs/boxo/ipld/unixfs" - unixfsio "github.com/ipfs/boxo/ipld/unixfs/io" - cid "github.com/ipfs/go-cid" - ipld "github.com/ipfs/go-ipld-format" -) - -// VerifyAll checks every final source file and the complete directory key set. -// Per-commit checks must not hide a lost untouched file or an extra binding. -func (s *StreamSession) VerifyAll(ctx context.Context) error { - if s == nil || s.failed || s.editor == nil { - return fmt.Errorf("invalid stream") - } - dag := merkledagimport.NewDAGService(s.store) - key, err := cid.Parse(s.editor.Root()) - if err != nil { - return err - } - node, err := dag.Get(ctx, key) - if err != nil { - return err - } - var paths []string - var walk func(ipld.Node, string) error - walk = func(node ipld.Node, prefix string) error { - dir, err := unixfsio.NewDirectoryFromNode(dag, node) - if err != nil { - return err - } - links, err := dir.Links(ctx) - if err != nil { - return err - } - for _, link := range links { - name := link.Name - if prefix != "" { - name = prefix + "/" + name - } - child, err := dag.Get(ctx, link.Cid) - if err != nil { - return err - } - isDirectory := false - if proto, ok := child.(*merkledag.ProtoNode); ok { - info, err := unixfs.FSNodeFromBytes(proto.Data()) - if err != nil { - return err - } - isDirectory = info.Type() == unixfs.TDirectory || info.Type() == unixfs.THAMTShard - } - if isDirectory && !s.flat { - if err := walk(child, name); err != nil { - return err - } - continue - } - if _, exists := s.state[name]; !exists { - return fmt.Errorf("unexpected final binding %q", name) - } - paths = append(paths, name) - } - return nil - } - if err := walk(node, ""); err != nil { - return err - } - sort.Strings(paths) - if len(paths) != len(s.state) { - return fmt.Errorf("final directory key set differs from frozen source") - } - for i := 1; i < len(paths); i++ { - if paths[i] == paths[i-1] { - return fmt.Errorf("duplicate final binding") - } - } - return s.VerifyPaths(ctx, paths) -} - -// RetainedCAS independently enumerates the actual CAS objects before cleanup. -// It measures serialized object bodies, excluding filesystem allocation and -// inode metadata; no physical-device write amplification is implied. -func (s *StreamSession) RetainedCAS() (objects, bytes int64, err error) { - if s == nil || s.failed || s.store == nil { - return 0, 0, fmt.Errorf("invalid stream") - } - s.store.mu.Lock() - defer s.store.mu.Unlock() - if s.store.archive != nil { - report, err := s.store.archive.inventory() - return report.Objects, report.BodyBytes, err - } - err = filepath.WalkDir(s.store.root, func(path string, entry os.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - if entry.IsDir() { - return nil - } - info, statErr := entry.Info() - if statErr != nil { - return statErr - } - if !info.Mode().IsRegular() { - return fmt.Errorf("non-regular CAS entry %q", path) - } - objects++ - bytes += info.Size() - return nil - }) - return -} - -// FinishReplayStorage reconciles all CAR frames with the disk index, closes -// the backend, and inventories its files before cleanup. This is terminal: -// further reads/mutations are rejected. It does not resume a failed replay. -func (s *StreamSession) FinishReplayStorage() (*ReplayStorage, error) { - if s == nil || s.failed || s.store == nil || s.store.archive == nil { - return nil, fmt.Errorf("invalid archive replay stream") - } - s.store.mu.Lock() - defer s.store.mu.Unlock() - report, err := s.store.archive.finish() - if err != nil { - s.failed = true - return nil, err - } - return &report, nil -} - -// VerifyPaths checks the active CAS root against the independently validated -// source state. It includes absence and verifies every traversed CID through -// the accounting store. It is not a MALT proof or a durable-write receipt. -func (s *StreamSession) VerifyPaths(ctx context.Context, paths []string) error { - if s == nil || s.failed || s.editor == nil { - return fmt.Errorf("readback requires an active, valid stream") - } - return s.verifyPaths(ctx, paths) -} - -func (s *StreamSession) verifyPaths(ctx context.Context, paths []string) error { - dag := merkledagimport.NewDAGService(s.store) - root, err := cid.Parse(s.editor.Root()) - if err != nil { - return err - } - for _, path := range paths { - if err := validateCanonicalPath("readback path", path); err != nil { - return err - } - node, err := dag.Get(ctx, root) - if err != nil { - return err - } - parts := strings.Split(path, "/") - want, exists := s.state[path] - if s.flat { - parts = []string{path} - } - for _, part := range parts { - dir, directoryErr := unixfsio.NewDirectoryFromNode(dag, node) - if directoryErr != nil { - // Replacing a directory with a regular file removes all former - // descendants. A CID-checked regular-file ancestor establishes - // absence; malformed or missing DAG nodes remain errors. - if !exists && !s.flat { - regular, kindErr := regularFileNode(node) - if kindErr != nil { - return kindErr - } - if regular { - err = os.ErrNotExist - break - } - } - return fmt.Errorf("readback %q: %w", path, directoryErr) - } - node, err = dir.Find(ctx, part) - if err != nil { - break - } - } - if !exists { - // Git can delete file p and add p/child in the same commit. The - // regular-file binding p is absent even though the derived directory - // p exists. Flat HAMT keys remain opaque and cannot use this rule. - if err == nil && !s.flat && s.hasSourceDescendant(path) { - if _, directoryErr := unixfsio.NewDirectoryFromNode(dag, node); directoryErr == nil { - continue - } - } - if !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("readback deleted path %q returned %v", path, err) - } - continue - } - if err != nil { - return fmt.Errorf("readback present path %q: %w", path, err) - } - if s.preserveMode { - proto, ok := node.(*merkledag.ProtoNode) - if !ok { - return fmt.Errorf("file %q lacks its declared permission metadata", path) - } - info, err := unixfs.FSNodeFromBytes(proto.Data()) - if err != nil { - return err - } - if uint32(info.Mode().Perm()) != want.mode { - return fmt.Errorf("readback permissions for %q differ from frozen source", path) - } - } - reader, err := unixfsio.NewDagReader(ctx, node, dag) - if err != nil { - return fmt.Errorf("readback file %q: %w", path, err) - } - hash := sha256.New() - n, readErr := io.Copy(hash, io.LimitReader(reader, int64(want.size)+1)) - closeErr := reader.Close() - if err := errors.Join(readErr, closeErr); err != nil { - return err - } - if n != int64(want.size) || hex.EncodeToString(hash.Sum(nil)) != want.hash { - return fmt.Errorf("readback %q does not match the frozen post-image", path) - } - } - return nil -} - -func (s *StreamSession) hasSourceDescendant(path string) bool { - prefix := path + "/" - for sourcePath := range s.state { - if strings.HasPrefix(sourcePath, prefix) { - return true - } - } - return false -} - -func regularFileNode(node ipld.Node) (bool, error) { - if _, ok := node.(*merkledag.RawNode); ok { - return true, nil - } - proto, ok := node.(*merkledag.ProtoNode) - if !ok { - return false, nil - } - info, err := unixfs.FSNodeFromBytes(proto.Data()) - if err != nil { - return false, err - } - return info.Type() == unixfs.TFile || info.Type() == unixfs.TRaw, nil -} diff --git a/internal/evaluation/rq3baseline/readback_test.go b/internal/evaluation/rq3baseline/readback_test.go deleted file mode 100644 index 453b59f..0000000 --- a/internal/evaluation/rq3baseline/readback_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package rq3baseline - -import ( - "context" - "testing" -) - -func TestFlatAndNestedHAMTUseDifferentPathIndexes(t *testing.T) { - ctx := context.Background() - mode, raw := uint32(0o644), true - file := frozenFile - spec := RunSpec{System: SystemHAMTUnixFS, Layout: LayoutSpec{Model: "unixfs", FileLayout: "balanced", DirectoryLayout: "hamt", Chunking: ChunkingSpec{Algorithm: "fixed", SizeBytes: 4}, HAMTFanout: 256, RawFileLeaf: &raw}, Snapshot: Snapshot{CommitID: "initial", Files: []FrozenFile{file("a/b.txt", []byte("one")), file("c.txt", []byte("two"))}}, Commits: []Commit{}} - nested, n, err := StartStream(ctx, spec) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if err := nested.Close(); err != nil { - t.Error(err) - } - }) - flat, f, err := StartFlatHAMTStream(ctx, spec) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if err := flat.Close(); err != nil { - t.Error(err) - } - }) - if n.Root == f.Root { - t.Fatal("flat and nested HAMT produced the same root") - } - for _, session := range []*StreamSession{nested, flat} { - if err := session.VerifyPaths(ctx, []string{"a/b.txt", "c.txt", "missing.txt"}); err != nil { - t.Fatal(err) - } - commit := Commit{CommitID: "delete", Mutations: []Mutation{{Kind: MutationDelete, FileKind: FileKindRegular, Path: "a/b.txt", ExpectedOldSHA256: digest([]byte("one")), ExpectedOldMode: &mode}}} - if _, err := session.ApplyChunkWithReadback(ctx, []Commit{commit}); err != nil { - t.Fatal(err) - } - if err := session.VerifyPaths(ctx, []string{"a/b.txt", "c.txt"}); err != nil { - t.Fatal(err) - } - if err := session.VerifyAll(ctx); err != nil { - t.Fatal(err) - } - } - var sum int64 - for _, event := range f.CAS.Events { - if event.ObjectRole == "" { - t.Fatal("CAS event lacks object ownership") - } - if event.Bytes != event.PayloadBytes+event.StructuralMetadataBytes { - t.Fatal("byte components do not partition object") - } - if event.Status == statusNewlyPersisted { - sum += event.Bytes - } - } - if sum != f.CAS.Total.NewlyPersistedBytes { - t.Fatal("event ledger does not reconcile") - } -} - -func TestReplayPreservesPermissionsForRawLeafFiles(t *testing.T) { - ctx := context.Background() - for _, flat := range []bool{false, true} { - spec := baseSpec(SystemHAMTUnixFS, "hamt", 256, []FrozenFile{frozenFile("a/b", []byte("one"))}) - session, initial, err := StartReplayStream(ctx, spec, flat) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if err := session.Close(); err != nil { - t.Error(err) - } - }) - if err := session.VerifyPaths(ctx, []string{"a/b"}); err != nil { - t.Fatal(err) - } - mode := uint32(0o755) - oldMode := uint32(0o644) - mutation := Mutation{Kind: MutationModeChange, Path: "a/b", FileKind: FileKindRegular, ExpectedOldSHA256: digest([]byte("one")), ExpectedOldMode: &oldMode, Mode: &mode} - records, err := session.ApplyChunkWithReadback(ctx, []Commit{{CommitID: "chmod", Mutations: []Mutation{mutation}}}) - if err != nil { - t.Fatal(err) - } - if records[0].Root == initial.Root || records[0].AdapterPayloadInputBytes != 0 || records[0].CAS.Total.NewlyPersistedBytes == 0 { - t.Fatal("mode-only update lost authenticated metadata or source boundary") - } - } -} - -func TestReplayReadbackDistinguishesFileAndDirectoryReplacements(t *testing.T) { - for _, profile := range []string{"merkledag-nested", "hamt-nested", "hamt-flat"} { - t.Run(profile, func(t *testing.T) { - mode := uint32(0o644) - system, directory, fanout := SystemHAMTUnixFS, "hamt", 256 - if profile == "merkledag-nested" { - system, directory, fanout = SystemMerkleDAGUnixFS, "basic", 0 - } - spec := baseSpec(system, directory, fanout, []FrozenFile{frozenFile("a", []byte("old-a")), frozenFile("b/c", []byte("old-c"))}) - session, _, err := StartReplayStream(t.Context(), spec, profile == "hamt-flat") - if err != nil { - t.Fatal(err) - } - defer session.Close() - insert := func(path, body string) Mutation { - file := frozenFile(path, []byte(body)) - return Mutation{Kind: MutationInsert, FileKind: FileKindRegular, Path: path, PayloadBase64: file.PayloadBase64, PayloadSHA256: file.PayloadSHA256, Mode: &mode} - } - remove := func(path, body string) Mutation { - return Mutation{Kind: MutationDelete, FileKind: FileKindRegular, Path: path, ExpectedOldSHA256: digest([]byte(body)), ExpectedOldMode: &mode} - } - commits := []Commit{ - {CommitID: "swap", Mutations: []Mutation{remove("a", "old-a"), insert("a/x", "new-x"), remove("b/c", "old-c"), insert("b", "new-b")}}, - {CommitID: "reverse", Mutations: []Mutation{remove("a/x", "new-x"), insert("a", "final-a"), remove("b", "new-b"), insert("b/c", "final-c")}}, - } - for _, commit := range commits { - if _, err := session.ApplyChunkWithReadback(t.Context(), []Commit{commit}); err != nil { - t.Fatal(err) - } - if err := session.VerifyAll(t.Context()); err != nil { - t.Fatal(err) - } - } - // Actual directory bindings still cannot stand in for expected files. - actual := session.state["b/c"] - session.state["b"] = actual - if err := session.VerifyPaths(t.Context(), []string{"b"}); err == nil { - t.Fatal("a directory was accepted as a source regular file") - } - delete(session.state, "b") - if err := session.VerifyPaths(t.Context(), []string{"a"}); err != nil { - t.Fatal(err) - } - }) - } -} diff --git a/internal/evaluation/rq3baseline/run.go b/internal/evaluation/rq3baseline/run.go deleted file mode 100644 index 3ba4715..0000000 --- a/internal/evaluation/rq3baseline/run.go +++ /dev/null @@ -1,247 +0,0 @@ -package rq3baseline - -import ( - "context" - "errors" - "fmt" - "io/fs" - "time" - - merkledagimport "github.com/dewebprotocol/malt-client/merkledag/importer" -) - -// Run validates the complete frozen workload before creating any CAS state, -// then executes it through the current incremental UnixFS Editor. -func Run(ctx context.Context, spec RunSpec) (_ *RunResult, returnErr error) { - prepared, err := prepare(spec) - if err != nil { - return nil, err - } - sourceAccounting := accountPreparedSource(prepared) - store := newAccountingStore() - defer func() { returnErr = errors.Join(returnErr, retryCleanup(store.close)) }() - editor, err := merkledagimport.NewEditor(store, importerOptions(prepared.spec)) - if err != nil { - return nil, fmt.Errorf("create UnixFS editor: %w", err) - } - - result := &RunResult{ - SchemaVersion: RunResultSchema, - CapabilityID: Capability().CapabilityID, - System: prepared.spec.System, - WorkloadSHA256: prepared.digest, - Layout: prepared.spec.Layout, - Records: make([]CommitRecord, 0, 1+len(prepared.commits)), - } - state := make(map[string]logicalFile, len(prepared.snapshot)) - - store.beginPhase() - started := time.Now() - snapshotMutations := make([]MutationExecution, 0, len(prepared.snapshot)) - var snapshotPayloadBytes int64 - for index, file := range prepared.snapshot { - if err := ctx.Err(); err != nil { - return nil, err - } - if err := editor.PutFile(ctx, file.path, file.data, fs.FileMode(file.mode)); err != nil { - return nil, fmt.Errorf("snapshot commit %q put %q: %w", prepared.spec.Snapshot.CommitID, file.path, err) - } - state[file.path] = logicalFile{data: cloneBytes(file.data), mode: file.mode, hash: file.hash, size: len(file.data)} - snapshotPayloadBytes += int64(len(file.data)) - snapshotMutations = append(snapshotMutations, MutationExecution{ - Index: index, - Kind: MutationInsert, - Path: file.path, - Translation: "snapshot_put_file", - LogicalObjectsChanged: 1, - LogicalBindingsChanged: 1, - LogicalPayloadBytes: int64(len(file.data)), - }) - } - snapshotElapsed := time.Since(started).Nanoseconds() - snapshotRoot := editor.Root() - if snapshotRoot == "" { - return nil, fmt.Errorf("snapshot commit %q produced an empty root", prepared.spec.Snapshot.CommitID) - } - snapshotCAS, snapshotPutNanos, snapshotGetNanos := store.finishPhase() - result.Records = append(result.Records, CommitRecord{ - CommitID: prepared.spec.Snapshot.CommitID, - Root: snapshotRoot, - Snapshot: true, - LogicalObjectsChanged: len(prepared.snapshot), - LogicalBindingsChanged: len(prepared.snapshot), - LogicalPayloadBytes: snapshotPayloadBytes, - AdapterPayloadInputBytes: sourceAccounting[0].AdapterPayloadInputBytes, - Mutations: snapshotMutations, - CAS: snapshotCAS, - ClientPhases: phaseMetrics(snapshotElapsed, snapshotPutNanos, snapshotGetNanos), - }) - - parentRoot := snapshotRoot - for commitIndex, commit := range prepared.commits { - store.beginPhase() - started = time.Now() - executions := make([]MutationExecution, 0, len(commit.mutations)) - logicalObjects := 0 - logicalBindings := 0 - var logicalPayload int64 - for index, mutation := range commit.mutations { - if err := ctx.Err(); err != nil { - return nil, err - } - execution, err := executeMutation(ctx, editor, state, index, mutation, nil) - if err != nil { - return nil, fmt.Errorf("commit %q mutation[%d]: %w", commit.id, index, err) - } - executions = append(executions, execution) - logicalObjects += execution.LogicalObjectsChanged - logicalBindings += execution.LogicalBindingsChanged - logicalPayload += execution.LogicalPayloadBytes - } - elapsed := time.Since(started).Nanoseconds() - root := editor.Root() - if root == "" { - return nil, fmt.Errorf("commit %q produced an empty root", commit.id) - } - casAccounting, putNanos, getNanos := store.finishPhase() - result.Records = append(result.Records, CommitRecord{ - CommitID: commit.id, - ParentRoot: parentRoot, - Root: root, - LogicalObjectsChanged: logicalObjects, - LogicalBindingsChanged: logicalBindings, - LogicalPayloadBytes: logicalPayload, - AdapterPayloadInputBytes: sourceAccounting[commitIndex+1].AdapterPayloadInputBytes, - Mutations: executions, - CAS: casAccounting, - ClientPhases: phaseMetrics(elapsed, putNanos, getNanos), - }) - parentRoot = root - } - return result, nil -} - -func importerOptions(spec RunSpec) merkledagimport.Options { - return merkledagimport.Options{ - Model: spec.Layout.Model, - FileLayout: spec.Layout.FileLayout, - DirLayout: spec.Layout.DirectoryLayout, - ChunkSize: spec.Layout.Chunking.SizeBytes, - HAMTFanout: spec.Layout.HAMTFanout, - RawFileLeaf: *spec.Layout.RawFileLeaf, - } -} - -func executeMutation(ctx context.Context, editor *merkledagimport.Editor, state map[string]logicalFile, index int, prepared preparedMutation, payloads *payloadStore) (MutationExecution, error) { - mutation := prepared.mutation - execution := MutationExecution{ - Index: index, - Kind: mutation.Kind, - Path: mutation.Path, - Destination: mutation.Destination, - LogicalObjectsChanged: 1, - } - switch mutation.Kind { - case MutationInsert: - if err := editor.PutFile(ctx, mutation.Path, prepared.data, fs.FileMode(*mutation.Mode)); err != nil { - return MutationExecution{}, err - } - retained, err := retainLogicalFile(prepared.data, *mutation.Mode, mutation.PayloadSHA256, payloads) - if err != nil { - return MutationExecution{}, err - } - state[mutation.Path] = retained - execution.Translation = "put_file" - execution.LogicalBindingsChanged = 1 - execution.LogicalPayloadBytes = int64(len(prepared.data)) - - case MutationReplace: - if err := editor.PutFile(ctx, mutation.Path, prepared.data, fs.FileMode(*mutation.Mode)); err != nil { - return MutationExecution{}, err - } - retained, err := retainLogicalFile(prepared.data, *mutation.Mode, mutation.PayloadSHA256, payloads) - if err != nil { - return MutationExecution{}, err - } - state[mutation.Path] = retained - execution.Translation = "put_file_replace" - execution.LogicalBindingsChanged = 1 - execution.LogicalPayloadBytes = int64(len(prepared.data)) - - case MutationAppend: - old := state[mutation.Path] - if err := editor.PutFile(ctx, mutation.Path, prepared.data, fs.FileMode(*mutation.Mode)); err != nil { - return MutationExecution{}, err - } - retained, err := retainLogicalFile(prepared.data, *mutation.Mode, mutation.PayloadSHA256, payloads) - if err != nil { - return MutationExecution{}, err - } - state[mutation.Path] = retained - execution.Translation = "put_file_full_result" - execution.LogicalBindingsChanged = 1 - execution.LogicalPayloadBytes = int64(len(prepared.data) - old.size) - - case MutationModeChange: - old := state[mutation.Path] - oldData, err := payloads.read(old) - if payloads == nil { - oldData, err = old.data, nil - } - if err != nil { - return MutationExecution{}, err - } - if err := editor.PutFile(ctx, mutation.Path, oldData, fs.FileMode(*mutation.Mode)); err != nil { - return MutationExecution{}, err - } - old.mode = *mutation.Mode - state[mutation.Path] = old - execution.Translation = "put_file_mode_change" - execution.LogicalBindingsChanged = 1 - - case MutationDelete: - if err := editor.RemoveFile(ctx, mutation.Path); err != nil { - return MutationExecution{}, err - } - delete(state, mutation.Path) - execution.Translation = "remove_file" - execution.LogicalBindingsChanged = 1 - - case MutationRename, MutationMove: - old := state[mutation.Path] - oldData, err := payloads.read(old) - if payloads == nil { - oldData, err = old.data, nil - } - if err != nil { - return MutationExecution{}, err - } - if err := editor.RemoveFile(ctx, mutation.Path); err != nil { - return MutationExecution{}, err - } - if err := editor.PutFile(ctx, mutation.Destination, oldData, fs.FileMode(old.mode)); err != nil { - return MutationExecution{}, err - } - delete(state, mutation.Path) - state[mutation.Destination] = old - execution.Translation = "remove_file_then_put_file" - execution.LogicalBindingsChanged = 2 - - default: - return MutationExecution{}, fmt.Errorf("unsupported mutation kind %q after validation", mutation.Kind) - } - return execution, nil -} - -func phaseMetrics(total, put, get int64) ClientPhases { - compute := total - put - get - if compute < 0 { - compute = 0 - } - return ClientPhases{ - ClientComputeWallNanos: total, - CASPutWallNanos: put, - CASGetWallNanos: get, - EditorOverheadExcludingCASWallNanos: compute, - } -} diff --git a/internal/evaluation/rq3baseline/run_test.go b/internal/evaluation/rq3baseline/run_test.go deleted file mode 100644 index f927163..0000000 --- a/internal/evaluation/rq3baseline/run_test.go +++ /dev/null @@ -1,629 +0,0 @@ -package rq3baseline - -import ( - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "errors" - "fmt" - "os" - "reflect" - "sort" - "strings" - "testing" - - casmemory "github.com/dewebprotocol/malt-client/internal/cas/memory" - merkledagimport "github.com/dewebprotocol/malt-client/merkledag/importer" - cid "github.com/ipfs/go-cid" -) - -func TestCleanupFailedPayloadFileReturnsCloseAndRemoveFailures(t *testing.T) { - primary := errors.New("write failed") - closeErr := errors.New("close failed") - removeErr := errors.New("remove failed") - got := cleanupFailedPayloadFile("payload", primary, closeErr, func(path string) error { - if path != "payload" { - t.Fatalf("remove path = %q", path) - } - return removeErr - }) - for _, want := range []error{primary, closeErr, removeErr} { - if !errors.Is(got, want) { - t.Fatalf("cleanup error %v does not include %v", got, want) - } - } - if got := cleanupFailedPayloadFile("payload", primary, nil, func(string) error { return os.ErrNotExist }); !errors.Is(got, primary) || errors.Is(got, os.ErrNotExist) { - t.Fatalf("not-exist cleanup should retain only the primary failure: %v", got) - } -} - -func TestPayloadStoreReconcileRetainsOnlyLiveCheckout(t *testing.T) { - store, err := newPayloadStore() - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if err := store.close(); err != nil { - t.Errorf("close payload store: %v", err) - } - }) - oldData, liveData := []byte("historical"), []byte("live") - oldSum, liveSum := sha256.Sum256(oldData), sha256.Sum256(liveData) - oldDigest, liveDigest := hex.EncodeToString(oldSum[:]), hex.EncodeToString(liveSum[:]) - if err := store.put(oldDigest, oldData); err != nil { - t.Fatal(err) - } - if err := store.put(liveDigest, liveData); err != nil { - t.Fatal(err) - } - live := map[string]logicalFile{"file": {hash: liveDigest, size: len(liveData)}} - if err := store.reconcile(live); err != nil { - t.Fatal(err) - } - if _, err := store.read(logicalFile{hash: oldDigest, size: len(oldData)}); !os.IsNotExist(err) { - t.Fatalf("historical payload read error = %v, want not-exist", err) - } - if got, err := store.read(live["file"]); err != nil || string(got) != string(liveData) { - t.Fatalf("live payload = %q, %v", got, err) - } -} - -func TestRunBasicUnixFSEmitsRootPerCommitAndExactDedupAccounting(t *testing.T) { - old := []byte("same-data") - appended := []byte("same-data-more") - spec := baseSpec(SystemMerkleDAGUnixFS, "basic", 0, []FrozenFile{ - frozenFile("docs/a.txt", old), - frozenFile("docs/b.txt", old), - }) - spec.Commits = []Commit{ - {CommitID: "c2-same-value", Mutations: []Mutation{replaceMutation("docs/a.txt", old, old)}}, - {CommitID: "c3-append", Mutations: []Mutation{appendMutation("docs/a.txt", old, appended)}}, - {CommitID: "c4-rename", Mutations: []Mutation{pathMutation(MutationRename, "docs/b.txt", "docs/c.txt", old)}}, - {CommitID: "c5-move", Mutations: []Mutation{pathMutation(MutationMove, "docs/c.txt", "archive/c.txt", old)}}, - {CommitID: "c6-delete", Mutations: []Mutation{pathMutation(MutationDelete, "archive/c.txt", "", old)}}, - } - - result, err := Run(t.Context(), spec) - if err != nil { - t.Fatal(err) - } - if len(result.Records) != 1+len(spec.Commits) { - t.Fatalf("records = %d, want %d", len(result.Records), 1+len(spec.Commits)) - } - if result.WorkloadSHA256 == "" || result.CapabilityID != Capability().CapabilityID { - t.Fatalf("missing provenance: %#v", result) - } - for i, record := range result.Records { - if _, err := cid.Parse(record.Root); err != nil { - t.Fatalf("record[%d] root %q: %v", i, record.Root, err) - } - if i > 0 && record.ParentRoot != result.Records[i-1].Root { - t.Fatalf("record[%d] parent = %q, want %q", i, record.ParentRoot, result.Records[i-1].Root) - } - if record.ClientPhases.ClientComputeWallNanos <= 0 { - t.Fatalf("record[%d] omitted client compute phase: %#v", i, record.ClientPhases) - } - assertAccountingAddsUp(t, record.CAS) - } - - snapshot := result.Records[0] - if got, want := snapshot.AdapterPayloadInputBytes, int64(len(old)*2); got != want { - t.Fatalf("snapshot adapter input bytes = %d, want full source snapshot %d", got, want) - } - if snapshot.CAS.PayloadChunks.NewlyPersistedBytes == 0 || snapshot.CAS.StructuralMetadata.NewlyPersistedBytes == 0 { - t.Fatalf("snapshot did not separate payload and structure: %#v", snapshot.CAS) - } - if snapshot.CAS.PayloadChunks.DuplicateObjects == 0 { - t.Fatalf("identical payload chunks were not identified as same-commit duplicates: %#v", snapshot.CAS.PayloadChunks) - } - - sameValue := result.Records[1] - if got, want := sameValue.AdapterPayloadInputBytes, int64(len(old)); got != want { - t.Fatalf("same-value replace adapter input bytes = %d, want full post-image %d", got, want) - } - if sameValue.Root != sameValue.ParentRoot { - t.Fatalf("same-value replace changed content-addressed root: parent=%s root=%s", sameValue.ParentRoot, sameValue.Root) - } - if sameValue.CAS.Total.NewlyPersistedObjects != 0 || sameValue.CAS.Total.AlreadyPresentObjects == 0 { - t.Fatalf("same-value replace accounting = %#v", sameValue.CAS.Total) - } - - appendRecord := result.Records[2] - if got, want := appendRecord.LogicalPayloadBytes, int64(len(appended)-len(old)); got != want { - t.Fatalf("append logical bytes = %d, want %d", got, want) - } - if appendRecord.CAS.Total.NewlyPersistedObjects == 0 { - t.Fatal("append persisted no new CAS objects") - } - if got, want := appendRecord.AdapterPayloadInputBytes, int64(len(appended)); got != want { - t.Fatalf("append adapter input bytes = %d, want full post-image %d", got, want) - } - - renameRecord := result.Records[3] - if renameRecord.LogicalPayloadBytes != 0 || renameRecord.LogicalBindingsChanged != 2 || renameRecord.Mutations[0].Translation != "remove_file_then_put_file" { - t.Fatalf("rename semantics = %#v", renameRecord) - } - if renameRecord.AdapterPayloadInputBytes != 0 || result.Records[4].AdapterPayloadInputBytes != 0 || result.Records[5].AdapterPayloadInputBytes != 0 { - t.Fatalf("path-only/delete adapter input bytes must be zero: rename=%d move=%d delete=%d", renameRecord.AdapterPayloadInputBytes, result.Records[4].AdapterPayloadInputBytes, result.Records[5].AdapterPayloadInputBytes) - } - if renameRecord.CAS.Total.AttemptedObjects == 0 { - t.Fatal("rename did not include intermediate Editor writes") - } - - second, err := Run(t.Context(), spec) - if err != nil { - t.Fatal(err) - } - if second.WorkloadSHA256 != result.WorkloadSHA256 { - t.Fatalf("workload digest changed: %s != %s", second.WorkloadSHA256, result.WorkloadSHA256) - } - for i := range result.Records { - if result.Records[i].Root != second.Records[i].Root || !reflect.DeepEqual(result.Records[i].CAS, second.Records[i].CAS) { - t.Fatalf("deterministic result mismatch at record %d", i) - } - } -} - -func TestStreamSessionMatchesSingleRunAcrossChunks(t *testing.T) { - payload := []byte("same-data") - spec := baseSpec(SystemMerkleDAGUnixFS, "basic", 0, []FrozenFile{ - frozenFile("docs/a.txt", payload), - frozenFile("docs/b.txt", payload), - }) - spec.Commits = []Commit{ - {CommitID: "c2-replace", Mutations: []Mutation{replaceMutation("docs/a.txt", payload, []byte("next"))}}, - {CommitID: "c3-rename", Mutations: []Mutation{pathMutation(MutationRename, "docs/b.txt", "docs/c.txt", payload)}}, - {CommitID: "c4-delete", Mutations: []Mutation{pathMutation(MutationDelete, "docs/c.txt", "", payload)}}, - } - - want, err := Run(t.Context(), spec) - if err != nil { - t.Fatal(err) - } - streamSpec := spec - streamSpec.Commits = []Commit{} - session, snapshot, err := StartStream(t.Context(), streamSpec) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if err := session.Close(); err != nil { - t.Errorf("close stream session: %v", err) - } - }) - for _, path := range []string{"docs/a.txt", "docs/b.txt"} { - if session.state[path].data != nil || session.validationState[path].data != nil { - t.Fatalf("stream retained %q payload bytes in live heap state", path) - } - stored, err := session.payloads.read(session.state[path]) - if err != nil || !reflect.DeepEqual(stored, payload) { - t.Fatalf("stream disk payload %q = %q, %v", path, stored, err) - } - } - first, err := session.ApplyChunk(t.Context(), spec.Commits[:1]) - if err != nil { - t.Fatal(err) - } - second, err := session.ApplyChunk(t.Context(), spec.Commits[1:]) - if err != nil { - t.Fatal(err) - } - got := append([]CommitRecord{snapshot}, first...) - got = append(got, second...) - if len(got) != len(want.Records) { - t.Fatalf("stream records = %d, want %d", len(got), len(want.Records)) - } - for index := range got { - got[index].ClientPhases = ClientPhases{} - want.Records[index].ClientPhases = ClientPhases{} - if !reflect.DeepEqual(got[index], want.Records[index]) { - t.Fatalf("stream record[%d] differs from single run:\n got: %#v\nwant: %#v", index, got[index], want.Records[index]) - } - } - if session.Root() != got[len(got)-1].Root { - t.Fatalf("stream root = %q, want final record root %q", session.Root(), got[len(got)-1].Root) - } -} - -func TestStreamSessionRejectsDuplicateCommitAcrossChunksWithoutAdvancing(t *testing.T) { - payload := []byte("payload") - spec := baseSpec(SystemMerkleDAGUnixFS, "basic", 0, []FrozenFile{frozenFile("file", payload)}) - session, _, err := StartStream(t.Context(), spec) - if err != nil { - t.Fatal(err) - } - commit := Commit{CommitID: "c2", Mutations: []Mutation{replaceMutation("file", payload, []byte("next"))}} - if _, err := session.ApplyChunk(t.Context(), []Commit{commit}); err != nil { - t.Fatal(err) - } - root := session.Root() - if _, err := session.ApplyChunk(t.Context(), []Commit{commit}); err == nil || !strings.Contains(err.Error(), "duplicate commit_id") { - t.Fatalf("duplicate commit error = %v", err) - } - if session.Root() != root { - t.Fatalf("duplicate commit advanced root from %q to %q", root, session.Root()) - } -} - -func TestOneShotAppendDirectlyAfterSnapshotCountsOnlySuffix(t *testing.T) { - old := []byte("base") - next := []byte("base-suffix") - spec := baseSpec(SystemMerkleDAGUnixFS, "basic", 0, []FrozenFile{frozenFile("file", old)}) - spec.Commits = []Commit{{CommitID: "append", Mutations: []Mutation{appendMutation("file", old, next)}}} - result, err := Run(t.Context(), spec) - if err != nil { - t.Fatal(err) - } - if got, want := result.Records[1].LogicalPayloadBytes, int64(len(next)-len(old)); got != want { - t.Fatalf("direct snapshot append logical bytes = %d, want suffix %d", got, want) - } -} - -func TestModeChangePreservesDiskBackedSizeAcrossAppendAndRename(t *testing.T) { - old := []byte("base") - next := []byte("base-suffix") - oldMode, newMode := uint32(0o644), uint32(0o755) - appendAfterMode := appendMutation("file", old, next) - appendAfterMode.ExpectedOldMode, appendAfterMode.Mode = &newMode, &newMode - renameAfterMode := pathMutation(MutationRename, "file", "renamed", next) - renameAfterMode.ExpectedOldMode, renameAfterMode.Mode = &newMode, &newMode - spec := baseSpec(SystemMerkleDAGUnixFS, "basic", 0, []FrozenFile{frozenFile("file", old)}) - spec.Commits = []Commit{ - {CommitID: "chmod", Mutations: []Mutation{{Kind: MutationModeChange, Path: "file", FileKind: FileKindRegular, ExpectedOldSHA256: digest(old), ExpectedOldMode: &oldMode, Mode: &newMode}}}, - {CommitID: "append", Mutations: []Mutation{appendAfterMode}}, - {CommitID: "rename", Mutations: []Mutation{renameAfterMode}}, - } - oneShot, err := Run(t.Context(), spec) - if err != nil { - t.Fatal(err) - } - if got, want := oneShot.Records[2].LogicalPayloadBytes, int64(len(next)-len(old)); got != want { - t.Fatalf("one-shot append after mode-change logical bytes = %d, want %d", got, want) - } - - streamSpec := spec - streamSpec.Commits = []Commit{} - session, _, err := StartStream(t.Context(), streamSpec) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if err := session.Close(); err != nil { - t.Errorf("close mode-change stream: %v", err) - } - }) - for index, commit := range spec.Commits { - records, err := session.ApplyChunk(t.Context(), []Commit{commit}) - if err != nil { - t.Fatalf("stream commit %d (%s): %v", index, commit.CommitID, err) - } - if commit.CommitID == "append" && records[0].LogicalPayloadBytes != int64(len(next)-len(old)) { - t.Fatalf("stream append after mode-change logical bytes = %d", records[0].LogicalPayloadBytes) - } - } -} - -func TestValidateAndAccountSourceDoesNotConflateAdapterInputWithLogicalDelta(t *testing.T) { - old := []byte("abcd") - next := []byte("abcdefgh") - spec := baseSpec(SystemMerkleDAGUnixFS, "basic", 0, []FrozenFile{frozenFile("file", old)}) - spec.Commits = []Commit{ - {CommitID: "append", Mutations: []Mutation{appendMutation("file", old, next)}}, - {CommitID: "rename", Mutations: []Mutation{pathMutation(MutationRename, "file", "renamed", next)}}, - } - accounting, err := ValidateAndAccountSource(spec) - if err != nil { - t.Fatal(err) - } - if len(accounting) != 3 || accounting[0].AdapterPayloadInputBytes != int64(len(old)) || - accounting[1].AdapterPayloadInputBytes != int64(len(next)) || accounting[2].AdapterPayloadInputBytes != 0 || - accounting[2].LogicalBindingsChanged != 2 { - t.Fatalf("source accounting = %#v", accounting) - } -} - -func TestRunHAMTUsesCurrentEditorAndAccountsMixedBatch(t *testing.T) { - files := make([]FrozenFile, 0, 32) - for i := 0; i < 32; i++ { - files = append(files, frozenFile(fmt.Sprintf("files/%02d.txt", i), []byte(fmt.Sprintf("payload-%02d", i)))) - } - sort.Slice(files, func(i, j int) bool { return files[i].Path < files[j].Path }) - spec := baseSpec(SystemHAMTUnixFS, "hamt", 16, files) - spec.Commits = []Commit{{CommitID: "hamt-mixed", Mutations: []Mutation{ - insertMutation("files/zz.txt", []byte("new")), - pathMutation(MutationDelete, "files/00.txt", "", []byte("payload-00")), - }}} - - result, err := Run(t.Context(), spec) - if err != nil { - t.Fatal(err) - } - if len(result.Records) != 2 || result.Records[1].Root == result.Records[0].Root { - t.Fatalf("HAMT roots = %#v", result.Records) - } - commit := result.Records[1] - if commit.LogicalObjectsChanged != 2 || commit.LogicalBindingsChanged != 2 { - t.Fatalf("HAMT mixed logical accounting = %#v", commit) - } - if commit.CAS.StructuralMetadata.AttemptedObjects == 0 || commit.CAS.Reads.Objects == 0 { - t.Fatalf("HAMT path did not expose structural writes/reads: %#v", commit.CAS) - } - for _, record := range result.Records { - for _, event := range record.CAS.Events { - if event.Category == categoryMixed { - t.Fatalf("raw-leaf HAMT bitfield was misclassified as payload: %#v", event) - } - } - } -} - -func TestRunSupportsCurrentTrickleFileLayout(t *testing.T) { - spec := baseSpec(SystemMerkleDAGUnixFS, "basic", 0, []FrozenFile{frozenFile("file.txt", []byte("0123456789abcdef"))}) - spec.Layout.FileLayout = "trickle" - result, err := Run(t.Context(), spec) - if err != nil { - t.Fatal(err) - } - if len(result.Records) != 1 || result.Records[0].CAS.PayloadChunks.NewlyPersistedObjects < 2 { - t.Fatalf("trickle result = %#v", result) - } -} - -func TestRunNonRawLeavesDecodeExactMixedByteAccounting(t *testing.T) { - payload := []byte("embedded-payload") - spec := baseSpec(SystemMerkleDAGUnixFS, "basic", 0, []FrozenFile{frozenFile("file.txt", payload)}) - raw := false - spec.Layout.RawFileLeaf = &raw - result, err := Run(t.Context(), spec) - if err != nil { - t.Fatal(err) - } - accounting := result.Records[0].CAS - foundMixed := false - for _, event := range accounting.Events { - if event.PayloadBytes+event.StructuralMetadataBytes != event.Bytes { - t.Fatalf("event byte split is not exact: %#v", event) - } - if event.Category == categoryMixed { - foundMixed = true - } - } - if !foundMixed || accounting.PayloadChunks.NewlyPersistedBytes == 0 || accounting.StructuralMetadata.NewlyPersistedBytes == 0 { - t.Fatalf("non-raw accounting = %#v", accounting) - } - imported, err := merkledagimport.ImportFiles(t.Context(), casmemory.New(), []merkledagimport.File{{Path: "file.txt", Data: payload, Mode: 0o644}}, merkledagimport.Options{ - Model: merkledagimport.ModelUnixFS, FileLayout: merkledagimport.FileLayoutBalanced, DirLayout: merkledagimport.DirLayoutBasic, ChunkSize: 4, RawFileLeaf: false, - }) - if err != nil { - t.Fatal(err) - } - if result.Records[0].Root != imported.Root { - t.Fatalf("Editor snapshot root %s differs from current ImportFiles root %s", result.Records[0].Root, imported.Root) - } -} - -func TestRunModeChangeMovesOnlyStructuralMetadata(t *testing.T) { - payload := []byte("unchanged") - spec := baseSpec(SystemMerkleDAGUnixFS, "basic", 0, []FrozenFile{frozenFile("script", payload)}) - oldMode, newMode := uint32(0o644), uint32(0o755) - spec.Commits = []Commit{{CommitID: "chmod", Mutations: []Mutation{{ - Kind: MutationModeChange, Path: "script", FileKind: FileKindRegular, ExpectedOldSHA256: digest(payload), ExpectedOldMode: &oldMode, Mode: &newMode, - }}}} - result, err := Run(t.Context(), spec) - if err != nil { - t.Fatal(err) - } - record := result.Records[1] - if record.Root == record.ParentRoot || record.LogicalPayloadBytes != 0 || record.Mutations[0].Translation != "put_file_mode_change" { - t.Fatalf("mode-change record = %#v", record) - } - if record.CAS.PayloadChunks.NewlyPersistedObjects != 0 || record.CAS.StructuralMetadata.NewlyPersistedObjects == 0 { - t.Fatalf("mode-change byte placement = %#v", record.CAS) - } -} - -func TestRunFailsClosedForCapabilityAndFrozenWorkloadViolations(t *testing.T) { - payload := []byte("payload") - tests := []struct { - name string - mutate func(*RunSpec) - wantGap string - wantErr string - }{ - { - name: "invalid HAMT fanout", - mutate: func(spec *RunSpec) { - spec.System = SystemHAMTUnixFS - spec.Layout.DirectoryLayout = "hamt" - spec.Layout.HAMTFanout = 2 - }, - wantErr: "multiple of 8", - }, - { - name: "directory rename", - mutate: func(spec *RunSpec) { - spec.Commits = []Commit{{CommitID: "c2", Mutations: []Mutation{pathMutation(MutationRename, "docs", "archive", payload)}}} - }, - wantGap: "directory_subtree_mutation", - }, - { - name: "symlink snapshot", - mutate: func(spec *RunSpec) { - spec.Snapshot.Files[0].FileKind = "symlink" - }, - wantGap: "symlink_and_special_file_mutation", - }, - { - name: "digest mismatch", - mutate: func(spec *RunSpec) { - spec.Snapshot.Files[0].PayloadSHA256 = strings.Repeat("0", 64) - }, - wantErr: "payload SHA-256 mismatch", - }, - { - name: "noncanonical base64", - mutate: func(spec *RunSpec) { - spec.Snapshot.Files[0].PayloadBase64 = base64.RawStdEncoding.EncodeToString(payload) - }, - wantErr: "not canonical standard base64", - }, - { - name: "unsorted snapshot", - mutate: func(spec *RunSpec) { - spec.Snapshot.Files = []FrozenFile{frozenFile("z", payload), frozenFile("a", payload)} - }, - wantErr: "strictly sorted", - }, - { - name: "stale old digest", - mutate: func(spec *RunSpec) { - mutation := replaceMutation("docs/file.txt", []byte("stale"), []byte("next")) - spec.Commits = []Commit{{CommitID: "c2", Mutations: []Mutation{mutation}}} - }, - wantErr: "old payload digest mismatch", - }, - { - name: "append is not prefix extension", - mutate: func(spec *RunSpec) { - spec.Commits = []Commit{{CommitID: "c2", Mutations: []Mutation{appendMutation("docs/file.txt", payload, []byte("other"))}}} - }, - wantErr: "strictly extend", - }, - { - name: "projected chunk output is bounded", - mutate: func(spec *RunSpec) { - spec.Layout.Chunking.SizeBytes = 1 - spec.Snapshot.Files = []FrozenFile{frozenFile("large", make([]byte, maxProjectedChunks+1))} - }, - wantErr: "projected payload chunks exceed", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - spec := baseSpec(SystemMerkleDAGUnixFS, "basic", 0, []FrozenFile{frozenFile("docs/file.txt", payload)}) - test.mutate(&spec) - _, err := Run(t.Context(), spec) - if err == nil { - t.Fatal("Run succeeded") - } - if test.wantGap != "" { - unsupported, ok := err.(*UnsupportedError) - if !ok || unsupported.Gap != test.wantGap { - t.Fatalf("error = %#v, want gap %q", err, test.wantGap) - } - } else if !strings.Contains(err.Error(), test.wantErr) { - t.Fatalf("error = %v, want substring %q", err, test.wantErr) - } - }) - } -} - -func TestCapabilityDocumentsFailClosedBoundaries(t *testing.T) { - capability := Capability() - if got, want := capability.SafetyLimits.MaximumJSONLRecordBytes, 512<<20; got != want { - t.Fatalf("maximum JSONL record bytes = %d, want %d", got, want) - } - if got, want := capability.SafetyLimits.MaximumDecodedPayloadBytes, 256<<20; got != want { - t.Fatalf("maximum decoded payload bytes = %d, want %d", got, want) - } - want := map[string]bool{ - "directory_subtree_mutation": false, - "physical_backend_bytes": false, - } - for _, gap := range capability.Gaps { - if !gap.FailClosed { - t.Errorf("capability gap %q is not fail closed", gap.Code) - } - if _, exists := want[gap.Code]; exists { - want[gap.Code] = gap.FailClosed - } - } - for code, failClosed := range want { - if !failClosed { - t.Errorf("capability does not declare fail-closed gap %q", code) - } - } -} - -func baseSpec(system, directoryLayout string, fanout int, files []FrozenFile) RunSpec { - raw := true - return RunSpec{ - System: system, - Layout: LayoutSpec{ - Model: "unixfs", - FileLayout: "balanced", - DirectoryLayout: directoryLayout, - Chunking: ChunkingSpec{Algorithm: "fixed", SizeBytes: 4}, - HAMTFanout: fanout, - RawFileLeaf: &raw, - }, - Snapshot: Snapshot{CommitID: "c1", Files: files}, - Commits: []Commit{}, - } -} - -func frozenFile(filePath string, data []byte) FrozenFile { - mode := uint32(0o644) - return FrozenFile{ - Path: filePath, - FileKind: FileKindRegular, - PayloadBase64: base64.StdEncoding.EncodeToString(data), - PayloadSHA256: digest(data), - Mode: &mode, - } -} - -func insertMutation(filePath string, data []byte) Mutation { - mode := uint32(0o644) - return Mutation{Kind: MutationInsert, Path: filePath, FileKind: FileKindRegular, PayloadBase64: base64.StdEncoding.EncodeToString(data), PayloadSHA256: digest(data), Mode: &mode} -} - -func replaceMutation(filePath string, old, next []byte) Mutation { - mutation := insertMutation(filePath, next) - mutation.Kind = MutationReplace - mutation.ExpectedOldSHA256 = digest(old) - expectedMode := uint32(0o644) - mutation.ExpectedOldMode = &expectedMode - return mutation -} - -func appendMutation(filePath string, old, next []byte) Mutation { - mutation := replaceMutation(filePath, old, next) - mutation.Kind = MutationAppend - return mutation -} - -func pathMutation(kind, source, destination string, old []byte) Mutation { - expectedMode := uint32(0o644) - mutation := Mutation{Kind: kind, Path: source, FileKind: FileKindRegular, Destination: destination, ExpectedOldSHA256: digest(old), ExpectedOldMode: &expectedMode} - if kind == MutationRename || kind == MutationMove { - mode := uint32(0o644) - mutation.Mode = &mode - } - return mutation -} - -func digest(data []byte) string { - sum := sha256.Sum256(data) - return hex.EncodeToString(sum[:]) -} - -func assertAccountingAddsUp(t *testing.T, accounting CASAccounting) { - t.Helper() - if accounting.Total.AttemptedObjects != len(accounting.Events) { - t.Fatalf("attempted objects = %d, events = %d", accounting.Total.AttemptedObjects, len(accounting.Events)) - } - if accounting.Total.AttemptedObjects != accounting.Total.NewlyPersistedObjects+accounting.Total.AlreadyPresentObjects+accounting.Total.DuplicateObjects { - t.Fatalf("status counts do not partition attempts: %#v", accounting.Total) - } - if accounting.Total.AttemptedBytes != accounting.Total.NewlyPersistedBytes+accounting.Total.AlreadyPresentBytes+accounting.Total.DuplicateBytes { - t.Fatalf("status bytes do not partition attempts: %#v", accounting.Total) - } - if accounting.Total.AttemptedBytes != accounting.PayloadChunks.AttemptedBytes+accounting.StructuralMetadata.AttemptedBytes || - accounting.Total.NewlyPersistedBytes != accounting.PayloadChunks.NewlyPersistedBytes+accounting.StructuralMetadata.NewlyPersistedBytes || - accounting.Total.AlreadyPresentBytes != accounting.PayloadChunks.AlreadyPresentBytes+accounting.StructuralMetadata.AlreadyPresentBytes || - accounting.Total.DuplicateBytes != accounting.PayloadChunks.DuplicateBytes+accounting.StructuralMetadata.DuplicateBytes { - t.Fatalf("byte categories do not partition attempts and statuses: %#v", accounting) - } -} diff --git a/internal/evaluation/rq3baseline/store.go b/internal/evaluation/rq3baseline/store.go deleted file mode 100644 index 04a14ba..0000000 --- a/internal/evaluation/rq3baseline/store.go +++ /dev/null @@ -1,319 +0,0 @@ -package rq3baseline - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "os" - "path/filepath" - "sync" - "time" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - merkledag "github.com/ipfs/boxo/ipld/merkledag" - unixfs "github.com/ipfs/boxo/ipld/unixfs" - blocks "github.com/ipfs/go-block-format" - cid "github.com/ipfs/go-cid" -) - -const ( - categoryPayloadChunk = "payload_chunk" - categoryStructural = "cas_structural_metadata" - categoryMixed = "mixed_payload_and_structural_metadata" - statusNewlyPersisted = "newly_persisted" - statusAlreadyPresent = "already_present" - statusDuplicateInCommit = "duplicate_in_commit" -) - -type accountingStore struct { - mu sync.Mutex - - root string - initErr error - closed bool - archive *archiveCAS - - phaseAttempts map[string]struct{} - events []CASWriteEvent - putNanos int64 - getNanos int64 - readObjects int - readBytes int64 -} - -func newAccountingStore() *accountingStore { - root, err := os.MkdirTemp("", "malt-rq3-logical-cas-") - return &accountingStore{root: root, initErr: err} -} - -func newReplayAccountingStore() *accountingStore { - s := newAccountingStore() - if s.initErr == nil { - s.archive, s.initErr = newArchiveCAS(s.root, archiveSegmentBytes) - } - return s -} - -func (s *accountingStore) readBlock(key cid.Cid) ([]byte, error) { - if s.archive != nil { - return s.archive.get(key) - } - return os.ReadFile(s.blockPath(key)) -} - -func (s *accountingStore) beginPhase() { - s.mu.Lock() - defer s.mu.Unlock() - s.phaseAttempts = make(map[string]struct{}) - s.events = make([]CASWriteEvent, 0) - s.putNanos = 0 - s.getNanos = 0 - s.readObjects = 0 - s.readBytes = 0 -} - -func (s *accountingStore) Get(ctx context.Context, key cid.Cid) ([]byte, error) { - started := time.Now() - if err := ctx.Err(); err != nil { - return nil, err - } - s.mu.Lock() - if s.initErr != nil { - err := s.initErr - s.mu.Unlock() - return nil, err - } - if s.closed { - s.mu.Unlock() - return nil, fmt.Errorf("evaluator CAS is closed") - } - data, err := s.readBlock(key) - if err == nil { - s.readObjects++ - s.readBytes += int64(len(data)) - } - s.getNanos += time.Since(started).Nanoseconds() - s.mu.Unlock() - if os.IsNotExist(err) { - return nil, fmt.Errorf("%w: %s", clientcas.ErrNotFound, key) - } - if err != nil { - return nil, err - } - computed, err := clientcas.CIDForBlock(clientcas.Block{Data: data, Codec: key.Type()}) - if err != nil || !computed.Equals(key) { - return nil, fmt.Errorf("disk-backed evaluator CAS block does not match CID %s", key) - } - return data, nil -} - -func (s *accountingStore) PutWithCodec(ctx context.Context, data []byte, codec uint64) (cid.Cid, error) { - started := time.Now() - if err := ctx.Err(); err != nil { - return cid.Undef, err - } - if codec != cid.Raw && codec != cid.DagProtobuf { - return cid.Undef, fmt.Errorf("unsupported evaluator CAS codec %d", codec) - } - key, err := clientcas.CIDForBlock(clientcas.Block{Data: data, Codec: codec}) - if err != nil { - return cid.Undef, err - } - category, payloadBytes, structuralBytes, err := classifyBlock(key, data, codec) - if err != nil { - return cid.Undef, err - } - role := "file_content" - if codec == cid.DagProtobuf { - node, decodeErr := merkledag.DecodeProtobuf(data) - if decodeErr != nil { - return cid.Undef, decodeErr - } - info, decodeErr := unixfs.FSNodeFromBytes(node.Data()) - if decodeErr != nil { - return cid.Undef, decodeErr - } - switch info.Type() { - case unixfs.TDirectory: - role = "directory_relation" - case unixfs.THAMTShard: - role = "hamt_relation" - } - } - - s.mu.Lock() - defer s.mu.Unlock() - defer func() { s.putNanos += time.Since(started).Nanoseconds() }() - if s.initErr != nil { - return cid.Undef, s.initErr - } - if s.closed { - return cid.Undef, fmt.Errorf("evaluator CAS is closed") - } - if s.phaseAttempts == nil { - return cid.Undef, fmt.Errorf("CAS accounting phase is not active") - } - keyString := key.String() - status := statusNewlyPersisted - objectPath := s.blockPath(key) - if existing, readErr := s.readBlock(key); readErr == nil { - if !bytes.Equal(existing, data) { - return cid.Undef, fmt.Errorf("CID collision for %s", key) - } - if _, attempted := s.phaseAttempts[keyString]; attempted { - status = statusDuplicateInCommit - } else { - status = statusAlreadyPresent - } - } else if !os.IsNotExist(readErr) { - return cid.Undef, readErr - } else if s.archive != nil { - if err := s.archive.appendNew(key, data); err != nil { - return cid.Undef, err - } - } else { - if err := os.MkdirAll(filepath.Dir(objectPath), 0o700); err != nil { - return cid.Undef, err - } - file, err := os.OpenFile(objectPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) - if err != nil { - return cid.Undef, err - } - _, writeErr := file.Write(data) - closeErr := file.Close() - if writeErr != nil || closeErr != nil { - return cid.Undef, cleanupFailedPayloadFile(objectPath, writeErr, closeErr, os.Remove) - } - } - s.phaseAttempts[keyString] = struct{}{} - s.events = append(s.events, CASWriteEvent{ - ObjectRole: role, - Sequence: len(s.events), - CID: keyString, - Codec: codec, - Category: category, - Bytes: int64(len(data)), - PayloadBytes: payloadBytes, - StructuralMetadataBytes: structuralBytes, - Status: status, - }) - return key, nil -} - -func (s *accountingStore) blockPath(key cid.Cid) string { - digest := sha256.Sum256(key.Bytes()) - name := hex.EncodeToString(digest[:]) - return filepath.Join(s.root, name[:2], name[2:4], name[4:]) -} - -func (s *accountingStore) close() error { - s.mu.Lock() - defer s.mu.Unlock() - s.closed = true - root := s.root - var closeErr error - if s.archive != nil { - closeErr = s.archive.closeHandles() - } - if root == "" { - return closeErr - } - if err := os.RemoveAll(root); err != nil { - return errors.Join(closeErr, err) - } - s.root = "" - return closeErr -} - -// retryCleanup gives one-shot and construction-failure paths a second owner -// attempt while close retains its path on failure. Active streams additionally -// keep their store handle for caller-driven retries. -func retryCleanup(close func() error) error { - if close == nil { - return nil - } - if err := close(); err != nil { - if retryErr := close(); retryErr != nil { - return errors.Join(err, retryErr) - } - } - return nil -} - -func (s *accountingStore) finishPhase() (CASAccounting, int64, int64) { - s.mu.Lock() - defer s.mu.Unlock() - accounting := CASAccounting{ - Events: append([]CASWriteEvent{}, s.events...), - Reads: CASReadAccounting{Objects: s.readObjects, Bytes: s.readBytes}, - } - for _, event := range accounting.Events { - accumulateCategory(&accounting.Total, event, event.Bytes) - if event.PayloadBytes > 0 || event.Category == categoryPayloadChunk { - accumulateCategory(&accounting.PayloadChunks, event, event.PayloadBytes) - } - if event.StructuralMetadataBytes > 0 { - accumulateCategory(&accounting.StructuralMetadata, event, event.StructuralMetadataBytes) - } - } - return accounting, s.putNanos, s.getNanos -} - -func accumulateCategory(summary *CategoryAccounting, event CASWriteEvent, bytes int64) { - summary.AttemptedObjects++ - summary.AttemptedBytes += bytes - switch event.Status { - case statusNewlyPersisted: - summary.NewlyPersistedObjects++ - summary.NewlyPersistedBytes += bytes - case statusAlreadyPresent: - summary.AlreadyPresentObjects++ - summary.AlreadyPresentBytes += bytes - case statusDuplicateInCommit: - summary.DuplicateObjects++ - summary.DuplicateBytes += bytes - } -} - -func classifyBlock(key cid.Cid, data []byte, codec uint64) (string, int64, int64, error) { - if codec == cid.Raw { - return categoryPayloadChunk, int64(len(data)), 0, nil - } - block, err := blocks.NewBlockWithCid(data, key) - if err != nil { - return "", 0, 0, fmt.Errorf("decode dag-pb accounting block %s: %w", key, err) - } - node, err := merkledag.DecodeProtobufBlock(block) - if err != nil { - return "", 0, 0, fmt.Errorf("decode dag-pb accounting node %s: %w", key, err) - } - protoNode, ok := node.(*merkledag.ProtoNode) - if !ok { - return "", 0, 0, fmt.Errorf("decoded dag-pb accounting node %s has type %T", key, node) - } - unixFSData, err := unixfs.FromBytes(protoNode.Data()) - if err != nil { - return "", 0, 0, fmt.Errorf("decode UnixFS accounting data %s: %w", key, err) - } - payloadBytes := int64(0) - switch unixFSData.GetType() { - case unixfs.TFile, unixfs.TRaw: - payloadBytes = int64(len(unixFSData.GetData())) - case unixfs.TDirectory, unixfs.THAMTShard: - // HAMT uses Data for its bitfield. It is authentication structure, - // not application payload, so the entire block remains metadata. - default: - return "", 0, 0, &UnsupportedError{Gap: "symlink_and_special_file_mutation", Message: fmt.Sprintf("UnixFS accounting node %s has unsupported type %d", key, unixFSData.GetType())} - } - structuralBytes := int64(len(data)) - payloadBytes - if structuralBytes < 0 { - return "", 0, 0, fmt.Errorf("UnixFS payload accounting exceeds dag-pb block %s", key) - } - if payloadBytes > 0 { - return categoryMixed, payloadBytes, structuralBytes, nil - } - return categoryStructural, 0, structuralBytes, nil -} diff --git a/internal/evaluation/rq3baseline/stream.go b/internal/evaluation/rq3baseline/stream.go deleted file mode 100644 index d919dac..0000000 --- a/internal/evaluation/rq3baseline/stream.go +++ /dev/null @@ -1,318 +0,0 @@ -package rq3baseline - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io/fs" - "time" - - merkledagimport "github.com/dewebprotocol/malt-client/merkledag/importer" -) - -// StreamSession retains one Editor and accounting CAS across bounded chunks. -type StreamSession struct { - spec RunSpec - store *accountingStore - editor *merkledagimport.Editor - state map[string]logicalFile - validationState map[string]logicalFile - payloads *payloadStore - seenCommits map[string]struct{} - parentRoot string - failed bool - commitIDs []string - flat bool - preserveMode bool -} - -// StartStream validates and applies one immutable initial snapshot. -func StartStream(ctx context.Context, spec RunSpec) (_ *StreamSession, _ CommitRecord, returnErr error) { - return startStream(ctx, spec, false, false) -} - -// StartFlatHAMTStream uses the same frozen source, file encoding and accounting -// store as StartStream, with full paths indexed in one HAMT instead of nested -// directories. The caller must publish this as a distinct adapter profile. -func StartFlatHAMTStream(ctx context.Context, spec RunSpec) (*StreamSession, CommitRecord, error) { - if spec.System != SystemHAMTUnixFS { - return nil, CommitRecord{}, fmt.Errorf("flat HAMT stream requires the HAMT file encoding profile") - } - return startStream(ctx, spec, true, false) -} - -// StartReplayStream preserves file permissions, including raw-leaf file roots. -// This is a new encoding profile, separate from the preserved RQ3 contract. -func StartReplayStream(ctx context.Context, spec RunSpec, flat bool) (*StreamSession, CommitRecord, error) { - if flat && spec.System != SystemHAMTUnixFS { - return nil, CommitRecord{}, fmt.Errorf("flat index requires HAMT") - } - return startStream(ctx, spec, flat, true) -} - -func startStream(ctx context.Context, spec RunSpec, flat, preserveMode bool) (_ *StreamSession, _ CommitRecord, returnErr error) { - factory := newAccountingStore - if preserveMode { - factory = newReplayAccountingStore - } - return startStreamWithStore(ctx, spec, flat, preserveMode, factory) -} - -func startStreamWithStore(ctx context.Context, spec RunSpec, flat, preserveMode bool, factory func() *accountingStore) (_ *StreamSession, _ CommitRecord, returnErr error) { - if spec.Commits == nil || len(spec.Commits) != 0 { - return nil, CommitRecord{}, fmt.Errorf("stream start requires an explicit empty commit chunk") - } - store := factory() - payloads, err := newPayloadStore() - if err != nil { - return nil, CommitRecord{}, errors.Join(err, retryCleanup(store.close)) - } - succeeded := false - defer func() { - if !succeeded { - returnErr = errors.Join(returnErr, retryCleanup(store.close), retryCleanup(payloads.close)) - } - }() - state, source, err := prepareStreamSnapshotWithEmpty(spec, payloads, preserveMode) - if err != nil { - return nil, CommitRecord{}, err - } - options := importerOptions(spec) - options.PreserveFileMode = preserveMode - editor, err := merkledagimport.NewEditor(store, options) - if flat { - editor, err = merkledagimport.NewFlatHAMTEditor(store, options) - } - if err != nil { - return nil, CommitRecord{}, fmt.Errorf("create UnixFS stream editor: %w", err) - } - minimal := RunSpec{System: spec.System, Layout: spec.Layout} - session := &StreamSession{spec: minimal, store: store, editor: editor, state: cloneLogicalState(state), validationState: state, payloads: payloads, seenCommits: map[string]struct{}{spec.Snapshot.CommitID: {}}, commitIDs: []string{spec.Snapshot.CommitID}, flat: flat, preserveMode: preserveMode} - if err := payloads.reconcile(session.state, session.validationState); err != nil { - return nil, CommitRecord{}, fmt.Errorf("reconcile hash stream snapshot payloads: %w", err) - } - store.beginPhase() - started := time.Now() - executions := make([]MutationExecution, 0, len(spec.Snapshot.Files)) - for index, frozen := range spec.Snapshot.Files { - if err := ctx.Err(); err != nil { - return nil, CommitRecord{}, err - } - file := state[frozen.Path] - data, err := payloads.read(file) - if err != nil { - return nil, CommitRecord{}, fmt.Errorf("snapshot commit %q read %q: %w", spec.Snapshot.CommitID, frozen.Path, err) - } - if err := editor.PutFile(ctx, frozen.Path, data, fs.FileMode(file.mode)); err != nil { - return nil, CommitRecord{}, fmt.Errorf("snapshot commit %q put %q: %w", spec.Snapshot.CommitID, frozen.Path, err) - } - executions = append(executions, MutationExecution{Index: index, Kind: MutationInsert, Path: frozen.Path, Translation: "snapshot_put_file", LogicalObjectsChanged: 1, LogicalBindingsChanged: 1, LogicalPayloadBytes: int64(len(data))}) - } - if len(state) == 0 { - if err := editor.EnsureRoot(ctx); err != nil { - return nil, CommitRecord{}, fmt.Errorf("materialize empty snapshot: %w", err) - } - } - elapsed := time.Since(started).Nanoseconds() - root := editor.Root() - if root == "" { - return nil, CommitRecord{}, fmt.Errorf("snapshot commit %q produced an empty root", spec.Snapshot.CommitID) - } - accounting, putNanos, getNanos := store.finishPhase() - session.parentRoot = root - succeeded = true - return session, CommitRecord{CommitID: spec.Snapshot.CommitID, Root: root, Snapshot: true, LogicalObjectsChanged: source.LogicalObjectsChanged, LogicalBindingsChanged: source.LogicalBindingsChanged, LogicalPayloadBytes: source.AdapterPayloadInputBytes, AdapterPayloadInputBytes: source.AdapterPayloadInputBytes, Mutations: executions, CAS: accounting, ClientPhases: phaseMetrics(elapsed, putNanos, getNanos)}, nil -} - -// ApplyChunk prevalidates a complete bounded chunk before mutating the Editor. -func (s *StreamSession) ApplyChunk(ctx context.Context, commits []Commit) ([]CommitRecord, error) { - return s.applyChunk(ctx, commits, false) -} - -// ApplyChunkWithReadback checks every touched path at its own commit root, -// before applying the next commit. Readback is excluded from write timings. -func (s *StreamSession) ApplyChunkWithReadback(ctx context.Context, commits []Commit) ([]CommitRecord, error) { - return s.applyChunk(ctx, commits, true) -} - -func (s *StreamSession) applyChunk(ctx context.Context, commits []Commit, readback bool) ([]CommitRecord, error) { - if s == nil || s.editor == nil || s.store == nil || s.failed { - return nil, fmt.Errorf("hash stream session is not initialized") - } - prepared, nextValidation, nextSeen, source, err := s.prepareChunk(commits) - if err != nil { - s.failed = true - return nil, err - } - s.failed = true - records := make([]CommitRecord, 0, len(prepared)) - for commitIndex, commit := range prepared { - s.store.beginPhase() - started := time.Now() - executions := make([]MutationExecution, 0, len(commit.mutations)) - logicalObjects, logicalBindings := 0, 0 - var logicalPayload int64 - for index, mutation := range commit.mutations { - if err := ctx.Err(); err != nil { - return nil, err - } - execution, err := executeMutation(ctx, s.editor, s.state, index, mutation, s.payloads) - if err != nil { - return nil, fmt.Errorf("commit %q mutation[%d]: %w", commit.id, index, err) - } - executions = append(executions, execution) - logicalObjects += execution.LogicalObjectsChanged - logicalBindings += execution.LogicalBindingsChanged - logicalPayload += execution.LogicalPayloadBytes - } - elapsed := time.Since(started).Nanoseconds() - root := s.editor.Root() - if root == "" { - return nil, fmt.Errorf("commit %q produced an empty root", commit.id) - } - accounting, putNanos, getNanos := s.store.finishPhase() - if readback { - paths := make([]string, 0, len(commit.mutations)*2) - for _, mutation := range commit.mutations { - paths = append(paths, mutation.mutation.Path) - if mutation.mutation.Destination != "" { - paths = append(paths, mutation.mutation.Destination) - } - } - if err := s.verifyPaths(ctx, paths); err != nil { - return nil, fmt.Errorf("commit %q: %w", commit.id, err) - } - } - records = append(records, CommitRecord{CommitID: commit.id, ParentRoot: s.parentRoot, Root: root, LogicalObjectsChanged: logicalObjects, LogicalBindingsChanged: logicalBindings, LogicalPayloadBytes: logicalPayload, AdapterPayloadInputBytes: source[commitIndex].AdapterPayloadInputBytes, Mutations: executions, CAS: accounting, ClientPhases: phaseMetrics(elapsed, putNanos, getNanos)}) - s.parentRoot = root - } - if err := s.payloads.reconcile(s.state, nextValidation); err != nil { - return nil, fmt.Errorf("reconcile hash stream chunk payloads: %w", err) - } - s.validationState, s.seenCommits = nextValidation, nextSeen - for _, commit := range commits { - s.commitIDs = append(s.commitIDs, commit.CommitID) - } - s.failed = false - return records, nil -} - -func (s *StreamSession) CommitListSHA256() (string, error) { - if s == nil || s.failed || len(s.commitIDs) == 0 { - return "", fmt.Errorf("hash stream commit list is unavailable") - } - encoded, err := json.Marshal(struct { - Commits []string `json:"commits"` - }{Commits: s.commitIDs}) - if err != nil { - return "", err - } - digest := sha256.Sum256(encoded) - return hex.EncodeToString(digest[:]), nil -} - -func (s *StreamSession) prepareChunk(commits []Commit) ([]preparedCommit, map[string]logicalFile, map[string]struct{}, []SourceCommitAccounting, error) { - return prepareStreamChunk(s.spec, s.validationState, s.seenCommits, commits, s.payloads) -} - -func prepareStreamChunk(spec RunSpec, validationState map[string]logicalFile, seenCommits map[string]struct{}, commits []Commit, payloads *payloadStore) ([]preparedCommit, map[string]logicalFile, map[string]struct{}, []SourceCommitAccounting, error) { - if commits == nil || len(commits) == 0 || len(commits) > maxCommits { - return nil, nil, nil, nil, fmt.Errorf("stream chunk requires 1..%d commits", maxCommits) - } - state := cloneLogicalState(validationState) - seen := make(map[string]struct{}, len(seenCommits)+len(commits)) - for commit := range seenCommits { - seen[commit] = struct{}{} - } - prepared := make([]preparedCommit, 0, len(commits)) - source := make([]SourceCommitAccounting, 0, len(commits)) - mutationCount, totalPayload, projected := 0, 0, 0 - for commitIndex, commit := range commits { - if err := validateCommitID(fmt.Sprintf("stream commits[%d].commit_id", commitIndex), commit.CommitID); err != nil { - return nil, nil, nil, nil, err - } - if _, duplicate := seen[commit.CommitID]; duplicate { - return nil, nil, nil, nil, fmt.Errorf("duplicate commit_id %q", commit.CommitID) - } - seen[commit.CommitID] = struct{}{} - if commit.Mutations == nil { - return nil, nil, nil, nil, fmt.Errorf("commit %q mutations must be an explicit array", commit.CommitID) - } - mutationCount += len(commit.Mutations) - if mutationCount > maxMutations { - return nil, nil, nil, nil, fmt.Errorf("stream chunk has more than %d mutations", maxMutations) - } - next := preparedCommit{id: commit.CommitID, mutations: make([]preparedMutation, 0, len(commit.Mutations))} - accounting := SourceCommitAccounting{CommitID: commit.CommitID, LogicalObjectsChanged: len(commit.Mutations)} - for mutationIndex, mutation := range commit.Mutations { - decoded, added, err := validateAndApplyMutation(state, commit.CommitID, mutationIndex, mutation, payloads) - if err != nil { - return nil, nil, nil, nil, err - } - totalPayload += added - if totalPayload > maxPayloadBytes { - return nil, nil, nil, nil, fmt.Errorf("stream chunk decoded payload bytes exceed %d", maxPayloadBytes) - } - if mutation.Kind == MutationInsert || mutation.Kind == MutationReplace || mutation.Kind == MutationAppend { - projected += projectedChunks(len(decoded), spec.Layout.Chunking.SizeBytes) - accounting.AdapterPayloadInputBytes += int64(len(decoded)) - } - if projected > maxProjectedChunks { - return nil, nil, nil, nil, fmt.Errorf("stream chunk projected payload chunks exceed %d", maxProjectedChunks) - } - if mutation.Kind == MutationRename || mutation.Kind == MutationMove { - accounting.LogicalBindingsChanged += 2 - } else { - accounting.LogicalBindingsChanged++ - } - next.mutations = append(next.mutations, preparedMutation{mutation: mutation, data: decoded}) - } - if len(state) > maxFiles { - return nil, nil, nil, nil, fmt.Errorf("stream state has more than %d files", maxFiles) - } - prepared = append(prepared, next) - source = append(source, accounting) - } - return prepared, state, seen, source, nil -} - -// Close releases the private disk-backed logical CAS retained by a stream. -func (s *StreamSession) Close() error { - if s == nil || s.store == nil && s.payloads == nil { - return nil - } - var storeErr, payloadErr error - if s.store != nil { - storeErr = s.store.close() - if storeErr == nil { - s.store = nil - } - } - if s.payloads != nil { - payloadErr = s.payloads.close() - if payloadErr == nil { - s.payloads = nil - } - } - s.editor = nil - s.failed = true - return errors.Join(storeErr, payloadErr) -} - -func cloneLogicalState(source map[string]logicalFile) map[string]logicalFile { - result := make(map[string]logicalFile, len(source)) - for path, file := range source { - result[path] = file - } - return result -} - -func (s *StreamSession) Root() string { - if s == nil { - return "" - } - return s.parentRoot -} diff --git a/internal/evaluation/rq3baseline/types.go b/internal/evaluation/rq3baseline/types.go deleted file mode 100644 index 29e8059..0000000 --- a/internal/evaluation/rq3baseline/types.go +++ /dev/null @@ -1,304 +0,0 @@ -// Package rq3baseline exposes the current hash-authenticated UnixFS write path -// as a narrow, deterministic evaluator primitive. It deliberately reports -// logical CAS accounting only; physical backend reconciliation belongs to the -// evaluator and gateway storage implementations. -package rq3baseline - -const ( - WorkerRequestSchema = "malt-rq3-hash-worker-request/v1" - WorkerResponseSchema = "malt-rq3-hash-worker-response/v1" - RunResultSchema = "malt-rq3-hash-run-result/v1" - CapabilitySchema = "malt-rq3-hash-capability/v1" - MaximumJSONLRecordBytes = 512 << 20 - - OperationCapabilities = "capabilities" - OperationRun = "run" - OperationStreamStart = "stream-start" - OperationStreamChunk = "stream-chunk" - OperationStreamFinish = "stream-finish" - - SystemMerkleDAGUnixFS = "merkledag-unixfs" - SystemHAMTUnixFS = "hamt-unixfs" - FileKindRegular = "regular" - - MutationInsert = "insert" - MutationReplace = "replace" - MutationAppend = "append" - MutationDelete = "delete" - MutationRename = "rename" - MutationMove = "move" - MutationModeChange = "mode-change" -) - -// WorkerRequest is one strict JSONL input record. Run must be absent for a -// capability request and present for a run request. -type WorkerRequest struct { - SchemaVersion string `json:"schema_version"` - RequestID string `json:"request_id"` - Operation string `json:"operation"` - Run *RunSpec `json:"run,omitempty"` -} - -type WorkerResponse struct { - SchemaVersion string `json:"schema_version"` - RequestID string `json:"request_id"` - OK bool `json:"ok"` - Capability *Capabilities `json:"capability,omitempty"` - Result *RunResult `json:"result,omitempty"` - Error *WorkerError `json:"error,omitempty"` - Stream *StreamResult `json:"stream,omitempty"` -} - -type StreamResult struct { - System string `json:"system"` - Layout LayoutSpec `json:"layout"` - Records []CommitRecord `json:"records"` - Root string `json:"root"` - CommitsApplied uint32 `json:"commits_applied"` - Complete bool `json:"complete"` - CommitListSHA256 string `json:"commit_list_sha256,omitempty"` -} - -type WorkerError struct { - Code string `json:"code"` - Message string `json:"message"` - CapabilityGap string `json:"capability_gap,omitempty"` -} - -// RunSpec keeps workload semantics separate from the selected hash baseline. -// Paths, payloads, modes, chunking, layouts, and commit boundaries are all -// explicit and therefore hashable before execution. -type RunSpec struct { - System string `json:"system"` - Layout LayoutSpec `json:"layout"` - Snapshot Snapshot `json:"snapshot"` - Commits []Commit `json:"commits"` -} - -type LayoutSpec struct { - Model string `json:"model"` - FileLayout string `json:"file_layout"` - DirectoryLayout string `json:"directory_layout"` - Chunking ChunkingSpec `json:"chunking"` - HAMTFanout int `json:"hamt_fanout"` - RawFileLeaf *bool `json:"raw_file_leaf"` -} - -type ChunkingSpec struct { - Algorithm string `json:"algorithm"` - SizeBytes int `json:"size_bytes"` -} - -type Snapshot struct { - CommitID string `json:"commit_id"` - Files []FrozenFile `json:"files"` -} - -type FrozenFile struct { - Path string `json:"path"` - FileKind string `json:"file_kind"` - PayloadBase64 string `json:"payload_base64"` - PayloadSHA256 string `json:"payload_sha256"` - Mode *uint32 `json:"mode"` -} - -type Commit struct { - CommitID string `json:"commit_id"` - Mutations []Mutation `json:"mutations"` -} - -// Mutation carries the complete resulting payload for insert, replace, and -// append. Append additionally proves that the previous payload is a prefix of -// the supplied result. Delete, rename, and move carry an expected old digest. -// Mode is the resulting mode for operations that leave a file present. -// ExpectedOldMode and ExpectedOldSHA256 freeze the prior file state for every -// operation other than insert. -type Mutation struct { - Kind string `json:"kind"` - Path string `json:"path"` - FileKind string `json:"file_kind"` - Destination string `json:"destination,omitempty"` - PayloadBase64 string `json:"payload_base64,omitempty"` - PayloadSHA256 string `json:"payload_sha256,omitempty"` - ExpectedOldSHA256 string `json:"expected_old_sha256,omitempty"` - ExpectedOldMode *uint32 `json:"expected_old_mode,omitempty"` - Mode *uint32 `json:"mode,omitempty"` -} - -type RunResult struct { - SchemaVersion string `json:"schema_version"` - CapabilityID string `json:"capability_id"` - System string `json:"system"` - WorkloadSHA256 string `json:"workload_sha256"` - Layout LayoutSpec `json:"layout"` - Records []CommitRecord `json:"records"` -} - -type CommitRecord struct { - CommitID string `json:"commit_id"` - ParentRoot string `json:"parent_root,omitempty"` - Root string `json:"root"` - Snapshot bool `json:"snapshot"` - LogicalObjectsChanged int `json:"logical_objects_changed"` - LogicalBindingsChanged int `json:"logical_bindings_changed"` - LogicalPayloadBytes int64 `json:"logical_payload_bytes"` - // AdapterPayloadInputBytes is the source envelope presented to the - // system-specific adapter: the complete snapshot bytes initially and the - // complete post-image for insert/replace/append. Path-only, delete, and - // mode-only mutations contribute zero. It is intentionally independent of - // LogicalPayloadBytes. - AdapterPayloadInputBytes int64 `json:"adapter_payload_input_bytes"` - Mutations []MutationExecution `json:"mutations"` - CAS CASAccounting `json:"cas"` - ClientPhases ClientPhases `json:"client_phases"` -} - -// SourceCommitAccounting is derived solely from the strict, system-neutral -// source envelope. It lets other adapters validate and account the exact same -// frozen workload without executing or warming the UnixFS baseline. -type SourceCommitAccounting struct { - CommitID string - LogicalObjectsChanged int - LogicalBindingsChanged int - AdapterPayloadInputBytes int64 -} - -type MutationExecution struct { - Index int `json:"index"` - Kind string `json:"kind"` - Path string `json:"path"` - Destination string `json:"destination,omitempty"` - Translation string `json:"translation"` - LogicalObjectsChanged int `json:"logical_objects_changed"` - LogicalBindingsChanged int `json:"logical_bindings_changed"` - LogicalPayloadBytes int64 `json:"logical_payload_bytes"` -} - -// CASAccounting counts Total objects once. A non-raw dag-pb leaf contributes -// one component-bearing object to both PayloadChunks and StructuralMetadata; -// therefore component object counts may overlap, while component byte counts -// always partition Total exactly. -type CASAccounting struct { - Events []CASWriteEvent `json:"events"` - Total CategoryAccounting `json:"total"` - PayloadChunks CategoryAccounting `json:"payload_chunks"` - StructuralMetadata CategoryAccounting `json:"structural_metadata"` - Reads CASReadAccounting `json:"reads"` -} - -type CASWriteEvent struct { - // ObjectRole is exported only by the separate replay-worker protocol. The - // preserved RQ3 wire format and its accounting categories stay unchanged. - ObjectRole string `json:"-"` - Sequence int `json:"sequence"` - CID string `json:"cid"` - Codec uint64 `json:"codec"` - Category string `json:"category"` - Bytes int64 `json:"bytes"` - PayloadBytes int64 `json:"payload_bytes"` - StructuralMetadataBytes int64 `json:"structural_metadata_bytes"` - Status string `json:"status"` -} - -type CategoryAccounting struct { - AttemptedObjects int `json:"attempted_objects"` - AttemptedBytes int64 `json:"attempted_bytes"` - NewlyPersistedObjects int `json:"newly_persisted_objects"` - NewlyPersistedBytes int64 `json:"newly_persisted_bytes"` - AlreadyPresentObjects int `json:"already_present_objects"` - AlreadyPresentBytes int64 `json:"already_present_bytes"` - DuplicateObjects int `json:"duplicate_objects"` - DuplicateBytes int64 `json:"duplicate_bytes"` -} - -type CASReadAccounting struct { - Objects int `json:"objects"` - Bytes int64 `json:"bytes"` -} - -// Durations are wall-clock measurements, not CPU time. ClientComputeWallNanos -// is the complete local Editor path. CAS timings are nested within it, and the -// final field is only the non-negative remainder after those calls. -type ClientPhases struct { - ClientComputeWallNanos int64 `json:"client_compute_wall_nanos"` - CASPutWallNanos int64 `json:"cas_put_wall_nanos"` - CASGetWallNanos int64 `json:"cas_get_wall_nanos"` - EditorOverheadExcludingCASWallNanos int64 `json:"editor_overhead_excluding_cas_wall_nanos"` -} - -type Capabilities struct { - SchemaVersion string `json:"schema_version"` - CapabilityID string `json:"capability_id"` - ExecutionPath string `json:"execution_path"` - Systems []string `json:"systems"` - FileLayouts []string `json:"file_layouts"` - DirectoryLayouts []string `json:"directory_layouts"` - MutationKinds []string `json:"mutation_kinds"` - AccountingCategories []string `json:"accounting_categories"` - WriteStatuses []string `json:"write_statuses"` - HistoricalRetention string `json:"historical_retention"` - ExactAccountingConditions []string `json:"exact_accounting_conditions"` - ExecutionNotes []string `json:"execution_notes"` - SafetyLimits CapabilityLimits `json:"safety_limits"` - Gaps []CapabilityGap `json:"gaps"` -} - -type CapabilityLimits struct { - MaximumJSONLRecordBytes int `json:"maximum_jsonl_record_bytes"` - MaximumFiles int `json:"maximum_files"` - MaximumCommits int `json:"maximum_commits"` - MaximumMutations int `json:"maximum_mutations"` - MaximumDecodedPayloadBytes int `json:"maximum_decoded_payload_bytes"` - MaximumProjectedChunks int `json:"maximum_projected_chunks"` - MaximumChunkSizeBytes int `json:"maximum_chunk_size_bytes"` -} - -type CapabilityGap struct { - Code string `json:"code"` - FailClosed bool `json:"fail_closed"` - Detail string `json:"detail"` -} - -func Capability() Capabilities { - return Capabilities{ - SchemaVersion: CapabilitySchema, - CapabilityID: "rq3.hash-unixfs-logical.v1", - ExecutionPath: "merkledag/importer.Editor", - Systems: []string{SystemMerkleDAGUnixFS, SystemHAMTUnixFS}, - FileLayouts: []string{"balanced", "trickle"}, - DirectoryLayouts: []string{"basic", "hamt"}, - MutationKinds: []string{MutationInsert, MutationReplace, MutationAppend, MutationDelete, MutationRename, MutationMove, MutationModeChange}, - AccountingCategories: []string{"payload_chunk", "cas_structural_metadata", "mixed_payload_and_structural_metadata"}, - WriteStatuses: []string{"newly_persisted", "already_present", "duplicate_in_commit"}, - HistoricalRetention: "all_blocks_for_all_emitted_roots", - ExactAccountingConditions: []string{ - "fixed-size chunking is explicit", - "HAMT fanout is an explicit power of two and multiple of 8 from 8 through 65536", - "raw_file_leaf is explicit; raw blocks are payload and dag-pb UnixFS nodes are decoded to split embedded payload from protobuf and link metadata", - "snapshot and mutations explicitly declare file_kind=regular, modes, and payload digests", - "mode is an unsigned Unix permission value from 0001 through 0777", - }, - ExecutionNotes: []string{ - "a commit emits one final root while all intermediate Editor writes remain included in attempted and newly-persisted accounting", - "content-addressed roots may repeat when a frozen commit is a semantic same-value operation", - "file rename and move execute as remove_file followed by put_file because that is the current Editor surface", - "Git regular modes 100644 and 100755 map to 0644 and 0755; Git symlink mode 120000 fails closed", - }, - SafetyLimits: CapabilityLimits{ - MaximumJSONLRecordBytes: MaximumJSONLRecordBytes, - MaximumFiles: maxFiles, - MaximumCommits: maxCommits, - MaximumMutations: maxMutations, - MaximumDecodedPayloadBytes: maxPayloadBytes, - MaximumProjectedChunks: maxProjectedChunks, - MaximumChunkSizeBytes: maxChunkSizeBytes, - }, - Gaps: []CapabilityGap{ - {Code: "directory_subtree_mutation", FailClosed: true, Detail: "the current incremental Editor has file put/remove but no directory or subtree rename/move API"}, - {Code: "symlink_and_special_file_mutation", FailClosed: true, Detail: "the frozen evaluator primitive accepts regular files only"}, - {Code: "empty_snapshot", FailClosed: true, Detail: "the current Editor cannot materialize an empty directory root before the first file put"}, - {Code: "external_root_resume", FailClosed: true, Detail: "the current Editor cannot initialize from an externally supplied root; each run imports an explicit frozen snapshot"}, - {Code: "physical_backend_bytes", FailClosed: true, Detail: "this primitive reports deterministic logical CAS events only; physical WAL/LSM reconciliation requires a storage-owned evaluator"}, - }, - } -} diff --git a/internal/evaluation/rq3baseline/validate.go b/internal/evaluation/rq3baseline/validate.go deleted file mode 100644 index 1707f51..0000000 --- a/internal/evaluation/rq3baseline/validate.go +++ /dev/null @@ -1,673 +0,0 @@ -package rq3baseline - -import ( - "bytes" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "path" - "strings" - "unicode/utf8" -) - -const ( - maxFiles = 1_000_000 - maxCommits = 4_096 - maxMutations = 65_536 - maxPayloadBytes = 256 << 20 - maxChunkSizeBytes = 16 << 20 - maxProjectedChunks = 250_000 - maxPathBytes = 4_096 - maxPathSegments = 256 - maxCommitIDBytes = 256 -) - -// UnsupportedError identifies a request that the current implementation -// cannot measure without inventing behavior. -type UnsupportedError struct { - Gap string - Message string -} - -func (e *UnsupportedError) Error() string { return e.Message } - -type preparedRun struct { - spec RunSpec - snapshot []preparedFile - commits []preparedCommit - digest string -} - -type preparedFile struct { - path string - data []byte - mode uint32 - hash string -} - -type preparedCommit struct { - id string - mutations []preparedMutation -} - -type preparedMutation struct { - mutation Mutation - data []byte -} - -type logicalFile struct { - data []byte - mode uint32 - hash string - size int -} - -// ValidationStreamSession performs only strict frozen-source validation and -// accounting. It retains the live logical tree, but creates no UnixFS Editor -// and no historical CAS, so another adapter can prevalidate without warming or -// duplicating a baseline implementation. -type ValidationStreamSession struct { - spec RunSpec - state map[string]logicalFile - seenCommits map[string]struct{} - payloads *payloadStore - failed bool -} - -func StartValidationStream(spec RunSpec) (_ *ValidationStreamSession, _ SourceCommitAccounting, returnErr error) { - if spec.Commits == nil || len(spec.Commits) != 0 { - return nil, SourceCommitAccounting{}, fmt.Errorf("validation stream start requires an explicit empty commit chunk") - } - payloads, err := newPayloadStore() - if err != nil { - return nil, SourceCommitAccounting{}, err - } - succeeded := false - defer func() { - if !succeeded { - returnErr = errors.Join(returnErr, retryCleanup(payloads.close)) - } - }() - state, source, err := prepareStreamSnapshot(spec, payloads) - if err != nil { - return nil, SourceCommitAccounting{}, err - } - if err := payloads.reconcile(state); err != nil { - return nil, SourceCommitAccounting{}, fmt.Errorf("reconcile validation snapshot payloads: %w", err) - } - minimal := RunSpec{System: spec.System, Layout: spec.Layout} - succeeded = true - return &ValidationStreamSession{spec: minimal, state: state, seenCommits: map[string]struct{}{spec.Snapshot.CommitID: {}}, payloads: payloads}, source, nil -} - -// prepareStreamSnapshot validates and spills one initial snapshot without -// ever retaining a preparedRun containing all decoded file bodies. The input -// request still owns its encoded corpus, while live semantic state contains -// only path/mode/digest/size metadata backed by the private payload store. -func prepareStreamSnapshot(spec RunSpec, payloads *payloadStore) (map[string]logicalFile, SourceCommitAccounting, error) { - return prepareStreamSnapshotWithEmpty(spec, payloads, false) -} - -// Empty initial trees belong to the new replay profile. The preserved RQ3 -// protocol retains its published unsupported-gap behavior. -func prepareStreamSnapshotWithEmpty(spec RunSpec, payloads *payloadStore, allowEmpty bool) (map[string]logicalFile, SourceCommitAccounting, error) { - if payloads == nil { - return nil, SourceCommitAccounting{}, fmt.Errorf("stream snapshot payload store is nil") - } - if err := validateLayout(spec.System, spec.Layout); err != nil { - return nil, SourceCommitAccounting{}, err - } - if err := validateCommitID("snapshot commit_id", spec.Snapshot.CommitID); err != nil { - return nil, SourceCommitAccounting{}, err - } - if spec.Snapshot.Files == nil { - return nil, SourceCommitAccounting{}, fmt.Errorf("snapshot files must be an explicit array") - } - if len(spec.Snapshot.Files) == 0 && !allowEmpty { - return nil, SourceCommitAccounting{}, &UnsupportedError{Gap: "empty_snapshot", Message: "empty snapshot is unsupported because the current Editor cannot materialize or resume an empty directory root"} - } - if len(spec.Snapshot.Files) > maxFiles { - return nil, SourceCommitAccounting{}, fmt.Errorf("snapshot has %d files; maximum is %d", len(spec.Snapshot.Files), maxFiles) - } - if spec.Commits == nil || len(spec.Commits) != 0 { - return nil, SourceCommitAccounting{}, fmt.Errorf("stream start commits must be an explicit empty array") - } - - state := make(map[string]logicalFile, len(spec.Snapshot.Files)) - totalPayload, totalProjectedChunks := 0, 0 - previousPath := "" - for index, file := range spec.Snapshot.Files { - if err := validateFileKind(fmt.Sprintf("snapshot files[%d].file_kind", index), file.FileKind); err != nil { - return nil, SourceCommitAccounting{}, err - } - if err := validateCanonicalPath(fmt.Sprintf("snapshot files[%d].path", index), file.Path); err != nil { - return nil, SourceCommitAccounting{}, err - } - if index > 0 && file.Path <= previousPath { - return nil, SourceCommitAccounting{}, fmt.Errorf("snapshot files must be strictly sorted by path; %q follows %q", file.Path, previousPath) - } - previousPath = file.Path - mode, err := validateMode(fmt.Sprintf("snapshot files[%d].mode", index), file.Mode) - if err != nil { - return nil, SourceCommitAccounting{}, err - } - data, err := decodePayload(fmt.Sprintf("snapshot files[%d]", index), file.PayloadBase64, file.PayloadSHA256) - if err != nil { - return nil, SourceCommitAccounting{}, err - } - totalPayload += len(data) - if totalPayload > maxPayloadBytes { - return nil, SourceCommitAccounting{}, fmt.Errorf("decoded payload bytes exceed %d", maxPayloadBytes) - } - totalProjectedChunks += projectedChunks(len(data), spec.Layout.Chunking.SizeBytes) - if totalProjectedChunks > maxProjectedChunks { - return nil, SourceCommitAccounting{}, fmt.Errorf("projected payload chunks exceed %d", maxProjectedChunks) - } - if conflict := conflictingPath(state, file.Path, ""); conflict != "" { - return nil, SourceCommitAccounting{}, fmt.Errorf("snapshot path %q conflicts with file path %q", file.Path, conflict) - } - retained, err := retainLogicalFile(data, mode, file.PayloadSHA256, payloads) - if err != nil { - return nil, SourceCommitAccounting{}, err - } - state[file.Path] = retained - } - return state, SourceCommitAccounting{ - CommitID: spec.Snapshot.CommitID, LogicalObjectsChanged: len(state), LogicalBindingsChanged: len(state), - AdapterPayloadInputBytes: int64(totalPayload), - }, nil -} - -func (s *ValidationStreamSession) ApplyChunk(commits []Commit) ([]SourceCommitAccounting, error) { - if s == nil || s.failed { - return nil, fmt.Errorf("validation stream is not active") - } - _, nextState, nextSeen, source, err := prepareStreamChunk(s.spec, s.state, s.seenCommits, commits, s.payloads) - if err != nil { - s.failed = true - return nil, err - } - if err := s.payloads.reconcile(nextState); err != nil { - s.failed = true - return nil, fmt.Errorf("reconcile validation chunk payloads: %w", err) - } - s.state, s.seenCommits = nextState, nextSeen - return source, nil -} - -// Close releases the disk-backed live checkout retained for semantic checks. -func (s *ValidationStreamSession) Close() error { - if s == nil { - return nil - } - s.failed = true - s.state = nil - err := s.payloads.close() - if err == nil { - s.payloads = nil - } - return err -} - -// ValidateAndAccountSource validates the entire frozen workload without -// creating an Editor or CAS state, and returns system-neutral per-commit -// source accounting. In particular, it does not execute the UnixFS baseline -// and is safe to use before another adapter's measured path. -func ValidateAndAccountSource(spec RunSpec) ([]SourceCommitAccounting, error) { - prepared, err := prepare(spec) - if err != nil { - return nil, err - } - return accountPreparedSource(prepared), nil -} - -func accountPreparedSource(prepared *preparedRun) []SourceCommitAccounting { - result := make([]SourceCommitAccounting, 0, len(prepared.commits)+1) - var snapshotBytes int64 - for _, file := range prepared.snapshot { - snapshotBytes += int64(len(file.data)) - } - result = append(result, SourceCommitAccounting{ - CommitID: prepared.spec.Snapshot.CommitID, LogicalObjectsChanged: len(prepared.snapshot), - LogicalBindingsChanged: len(prepared.snapshot), AdapterPayloadInputBytes: snapshotBytes, - }) - for _, commit := range prepared.commits { - accounting := SourceCommitAccounting{CommitID: commit.id, LogicalObjectsChanged: len(commit.mutations)} - for _, mutation := range commit.mutations { - switch mutation.mutation.Kind { - case MutationRename, MutationMove: - accounting.LogicalBindingsChanged += 2 - default: - accounting.LogicalBindingsChanged++ - } - switch mutation.mutation.Kind { - case MutationInsert, MutationReplace, MutationAppend: - accounting.AdapterPayloadInputBytes += int64(len(mutation.data)) - } - } - result = append(result, accounting) - } - return result -} - -func prepare(spec RunSpec) (*preparedRun, error) { - if err := validateLayout(spec.System, spec.Layout); err != nil { - return nil, err - } - if err := validateCommitID("snapshot commit_id", spec.Snapshot.CommitID); err != nil { - return nil, err - } - if spec.Snapshot.Files == nil { - return nil, fmt.Errorf("snapshot files must be an explicit array") - } - if len(spec.Snapshot.Files) == 0 { - return nil, &UnsupportedError{Gap: "empty_snapshot", Message: "empty snapshot is unsupported because the current Editor cannot materialize or resume an empty directory root"} - } - if len(spec.Snapshot.Files) > maxFiles { - return nil, fmt.Errorf("snapshot has %d files; maximum is %d", len(spec.Snapshot.Files), maxFiles) - } - if spec.Commits == nil { - return nil, fmt.Errorf("commits must be an explicit array") - } - if len(spec.Commits) > maxCommits { - return nil, fmt.Errorf("run has %d commits; maximum is %d", len(spec.Commits), maxCommits) - } - - state := make(map[string]logicalFile, len(spec.Snapshot.Files)) - preparedSnapshot := make([]preparedFile, 0, len(spec.Snapshot.Files)) - totalPayload := 0 - totalProjectedChunks := 0 - previousPath := "" - for i, file := range spec.Snapshot.Files { - if err := validateFileKind(fmt.Sprintf("snapshot files[%d].file_kind", i), file.FileKind); err != nil { - return nil, err - } - if err := validateCanonicalPath(fmt.Sprintf("snapshot files[%d].path", i), file.Path); err != nil { - return nil, err - } - if i > 0 && file.Path <= previousPath { - return nil, fmt.Errorf("snapshot files must be strictly sorted by path; %q follows %q", file.Path, previousPath) - } - previousPath = file.Path - mode, err := validateMode(fmt.Sprintf("snapshot files[%d].mode", i), file.Mode) - if err != nil { - return nil, err - } - data, err := decodePayload(fmt.Sprintf("snapshot files[%d]", i), file.PayloadBase64, file.PayloadSHA256) - if err != nil { - return nil, err - } - totalPayload += len(data) - if totalPayload > maxPayloadBytes { - return nil, fmt.Errorf("decoded payload bytes exceed %d", maxPayloadBytes) - } - totalProjectedChunks += projectedChunks(len(data), spec.Layout.Chunking.SizeBytes) - if totalProjectedChunks > maxProjectedChunks { - return nil, fmt.Errorf("projected payload chunks exceed %d", maxProjectedChunks) - } - if conflict := conflictingPath(state, file.Path, ""); conflict != "" { - return nil, fmt.Errorf("snapshot path %q conflicts with file path %q", file.Path, conflict) - } - state[file.Path] = logicalFile{data: cloneBytes(data), mode: mode, hash: file.PayloadSHA256, size: len(data)} - preparedSnapshot = append(preparedSnapshot, preparedFile{path: file.Path, data: data, mode: mode, hash: file.PayloadSHA256}) - } - - seenCommits := map[string]struct{}{spec.Snapshot.CommitID: {}} - preparedCommits := make([]preparedCommit, 0, len(spec.Commits)) - mutationCount := 0 - for commitIndex, commit := range spec.Commits { - if err := validateCommitID(fmt.Sprintf("commits[%d].commit_id", commitIndex), commit.CommitID); err != nil { - return nil, err - } - if _, exists := seenCommits[commit.CommitID]; exists { - return nil, fmt.Errorf("duplicate commit_id %q", commit.CommitID) - } - seenCommits[commit.CommitID] = struct{}{} - if commit.Mutations == nil { - return nil, fmt.Errorf("commit %q mutations must be an explicit array", commit.CommitID) - } - mutationCount += len(commit.Mutations) - if mutationCount > maxMutations { - return nil, fmt.Errorf("run has more than %d mutations", maxMutations) - } - prepared := preparedCommit{id: commit.CommitID, mutations: make([]preparedMutation, 0, len(commit.Mutations))} - for mutationIndex, mutation := range commit.Mutations { - decoded, addedPayload, err := validateAndApplyMutation(state, commit.CommitID, mutationIndex, mutation, nil) - if err != nil { - return nil, err - } - totalPayload += addedPayload - if totalPayload > maxPayloadBytes { - return nil, fmt.Errorf("decoded payload bytes exceed %d", maxPayloadBytes) - } - if mutation.Kind == MutationInsert || mutation.Kind == MutationReplace || mutation.Kind == MutationAppend { - totalProjectedChunks += projectedChunks(len(decoded), spec.Layout.Chunking.SizeBytes) - if totalProjectedChunks > maxProjectedChunks { - return nil, fmt.Errorf("projected payload chunks exceed %d", maxProjectedChunks) - } - } - prepared.mutations = append(prepared.mutations, preparedMutation{mutation: mutation, data: decoded}) - } - preparedCommits = append(preparedCommits, prepared) - } - - encoded, err := json.Marshal(spec) - if err != nil { - return nil, fmt.Errorf("encode canonical workload: %w", err) - } - digest := sha256.Sum256(encoded) - return &preparedRun{ - spec: spec, - snapshot: preparedSnapshot, - commits: preparedCommits, - digest: hex.EncodeToString(digest[:]), - }, nil -} - -func validateLayout(system string, layout LayoutSpec) error { - if layout.Model != "unixfs" { - return fmt.Errorf("layout model must be %q", "unixfs") - } - if layout.FileLayout != "balanced" && layout.FileLayout != "trickle" { - return fmt.Errorf("unsupported file_layout %q", layout.FileLayout) - } - if layout.Chunking.Algorithm != "fixed" { - return fmt.Errorf("chunking algorithm must be %q", "fixed") - } - if layout.Chunking.SizeBytes <= 0 || layout.Chunking.SizeBytes > maxChunkSizeBytes { - return fmt.Errorf("chunking size_bytes must be in [1,%d]", maxChunkSizeBytes) - } - if layout.RawFileLeaf == nil { - return fmt.Errorf("raw_file_leaf must be explicit") - } - switch system { - case SystemMerkleDAGUnixFS: - if layout.DirectoryLayout != "basic" { - return fmt.Errorf("system %q requires directory_layout %q", system, "basic") - } - if layout.HAMTFanout != 0 { - return fmt.Errorf("system %q requires hamt_fanout=0", system) - } - case SystemHAMTUnixFS: - if layout.DirectoryLayout != "hamt" { - return fmt.Errorf("system %q requires directory_layout %q", system, "hamt") - } - if layout.HAMTFanout < 8 || layout.HAMTFanout > 65_536 || layout.HAMTFanout%8 != 0 || layout.HAMTFanout&(layout.HAMTFanout-1) != 0 { - return fmt.Errorf("hamt_fanout must be a power of two and multiple of 8 in [8,65536]") - } - default: - return fmt.Errorf("unsupported system %q", system) - } - return nil -} - -func validateAndApplyMutation(state map[string]logicalFile, commitID string, index int, mutation Mutation, store *payloadStore) ([]byte, int, error) { - label := fmt.Sprintf("commit %q mutation[%d]", commitID, index) - if err := validateFileKind(label+".file_kind", mutation.FileKind); err != nil { - return nil, 0, err - } - if err := validateCanonicalPath(label+".path", mutation.Path); err != nil { - return nil, 0, err - } - old, exists := state[mutation.Path] - switch mutation.Kind { - case MutationInsert: - if mutation.Destination != "" || mutation.ExpectedOldSHA256 != "" || mutation.ExpectedOldMode != nil { - return nil, 0, fmt.Errorf("%s insert has fields that are not applicable", label) - } - mode, err := validateMode(label+".mode", mutation.Mode) - if err != nil { - return nil, 0, err - } - if exists { - return nil, 0, fmt.Errorf("%s insert target already exists", label) - } - if conflict := conflictingPath(state, mutation.Path, ""); conflict != "" { - return nil, 0, fmt.Errorf("%s insert path conflicts with file %q", label, conflict) - } - data, err := decodePayload(label, mutation.PayloadBase64, mutation.PayloadSHA256) - if err != nil { - return nil, 0, err - } - retained, err := retainLogicalFile(data, mode, mutation.PayloadSHA256, store) - if err != nil { - return nil, 0, err - } - state[mutation.Path] = retained - return data, len(data), nil - - case MutationReplace, MutationAppend: - if mutation.Destination != "" { - return nil, 0, fmt.Errorf("%s %s has a destination", label, mutation.Kind) - } - mode, err := validateMode(label+".mode", mutation.Mode) - if err != nil { - return nil, 0, err - } - expectedMode, err := validateMode(label+".expected_old_mode", mutation.ExpectedOldMode) - if err != nil { - return nil, 0, err - } - if err := requireOldFile(state, mutation.Path, old, exists, mutation.ExpectedOldSHA256, expectedMode, label); err != nil { - return nil, 0, err - } - data, err := decodePayload(label, mutation.PayloadBase64, mutation.PayloadSHA256) - if err != nil { - return nil, 0, err - } - if mutation.Kind == MutationAppend { - oldData, err := store.read(old) - if store == nil { - oldData, err = old.data, nil - } - if err != nil { - return nil, 0, fmt.Errorf("%s read old append payload: %w", label, err) - } - if len(data) <= len(oldData) || !bytes.Equal(data[:len(oldData)], oldData) { - return nil, 0, fmt.Errorf("%s append payload must strictly extend the old payload", label) - } - } - retained, err := retainLogicalFile(data, mode, mutation.PayloadSHA256, store) - if err != nil { - return nil, 0, err - } - state[mutation.Path] = retained - return data, len(data), nil - - case MutationModeChange: - if mutation.Destination != "" || mutation.PayloadBase64 != "" || mutation.PayloadSHA256 != "" { - return nil, 0, fmt.Errorf("%s mode-change has fields that are not applicable", label) - } - mode, err := validateMode(label+".mode", mutation.Mode) - if err != nil { - return nil, 0, err - } - expectedMode, err := validateMode(label+".expected_old_mode", mutation.ExpectedOldMode) - if err != nil { - return nil, 0, err - } - if err := requireOldFile(state, mutation.Path, old, exists, mutation.ExpectedOldSHA256, expectedMode, label); err != nil { - return nil, 0, err - } - if mode == expectedMode { - return nil, 0, fmt.Errorf("%s mode-change must change the mode", label) - } - old.mode = mode - state[mutation.Path] = old - return nil, 0, nil - - case MutationDelete: - if mutation.Destination != "" || mutation.PayloadBase64 != "" || mutation.PayloadSHA256 != "" || mutation.Mode != nil { - return nil, 0, fmt.Errorf("%s delete has fields that are not applicable", label) - } - expectedMode, err := validateMode(label+".expected_old_mode", mutation.ExpectedOldMode) - if err != nil { - return nil, 0, err - } - if err := requireOldFile(state, mutation.Path, old, exists, mutation.ExpectedOldSHA256, expectedMode, label); err != nil { - return nil, 0, err - } - delete(state, mutation.Path) - return nil, 0, nil - - case MutationRename, MutationMove: - if mutation.PayloadBase64 != "" || mutation.PayloadSHA256 != "" { - return nil, 0, fmt.Errorf("%s %s has payload fields", label, mutation.Kind) - } - mode, err := validateMode(label+".mode", mutation.Mode) - if err != nil { - return nil, 0, err - } - expectedMode, err := validateMode(label+".expected_old_mode", mutation.ExpectedOldMode) - if err != nil { - return nil, 0, err - } - if mode != expectedMode { - return nil, 0, fmt.Errorf("%s %s cannot also change file mode", label, mutation.Kind) - } - if err := requireOldFile(state, mutation.Path, old, exists, mutation.ExpectedOldSHA256, expectedMode, label); err != nil { - return nil, 0, err - } - if err := validateCanonicalPath(label+".destination", mutation.Destination); err != nil { - return nil, 0, err - } - if mutation.Destination == mutation.Path { - return nil, 0, fmt.Errorf("%s destination equals source", label) - } - if mutation.Kind == MutationRename && path.Dir(mutation.Path) != path.Dir(mutation.Destination) { - return nil, 0, fmt.Errorf("%s rename must remain in the same parent directory; use move", label) - } - if mutation.Kind == MutationMove && path.Dir(mutation.Path) == path.Dir(mutation.Destination) { - return nil, 0, fmt.Errorf("%s move must change parent directory; use rename", label) - } - if _, destinationExists := state[mutation.Destination]; destinationExists { - return nil, 0, fmt.Errorf("%s destination already exists", label) - } - if conflict := conflictingPath(state, mutation.Destination, mutation.Path); conflict != "" { - return nil, 0, fmt.Errorf("%s destination conflicts with file %q", label, conflict) - } - delete(state, mutation.Path) - state[mutation.Destination] = old - return nil, 0, nil - - default: - return nil, 0, fmt.Errorf("%s has unsupported kind %q", label, mutation.Kind) - } -} - -func requireOldFile(state map[string]logicalFile, filePath string, old logicalFile, exists bool, digest string, mode uint32, label string) error { - if !exists { - for existing := range state { - if strings.HasPrefix(existing, filePath+"/") { - return &UnsupportedError{Gap: "directory_subtree_mutation", Message: fmt.Sprintf("%s selects directory or subtree %q", label, filePath)} - } - } - return fmt.Errorf("%s source does not exist", label) - } - if err := validateDigest(label+".expected_old_sha256", digest); err != nil { - return err - } - if old.hash != digest { - return fmt.Errorf("%s old payload digest mismatch", label) - } - if old.mode != mode { - return fmt.Errorf("%s old mode mismatch: got %04o, expected %04o", label, mode, old.mode) - } - return nil -} - -func validateCanonicalPath(label, raw string) error { - if raw == "" || len(raw) > maxPathBytes || !utf8.ValidString(raw) || strings.ContainsRune(raw, '\x00') || strings.ContainsRune(raw, '\\') { - return fmt.Errorf("%s is not a valid UTF-8 relative path", label) - } - if strings.Count(raw, "/")+1 > maxPathSegments { - return fmt.Errorf("%s has more than %d path segments", label, maxPathSegments) - } - clean := path.Clean(raw) - if raw != clean || clean == "." || clean == ".." || strings.HasPrefix(clean, "../") || strings.HasPrefix(clean, "/") { - return fmt.Errorf("%s must be a canonical relative path", label) - } - return nil -} - -func validateMode(label string, mode *uint32) (uint32, error) { - if mode == nil { - return 0, fmt.Errorf("%s must be explicit", label) - } - if *mode == 0 || *mode > 0o777 { - return 0, fmt.Errorf("%s must contain only non-zero Unix permission bits", label) - } - return *mode, nil -} - -func validateFileKind(label, kind string) error { - if kind == "" { - return fmt.Errorf("%s must be explicit", label) - } - if kind != FileKindRegular { - return &UnsupportedError{Gap: "symlink_and_special_file_mutation", Message: fmt.Sprintf("%s %q is unsupported; only regular files can use the current Editor primitive", label, kind)} - } - return nil -} - -func validateCommitID(label, id string) error { - if strings.TrimSpace(id) != id || id == "" || len(id) > maxCommitIDBytes || !utf8.ValidString(id) { - return fmt.Errorf("%s must be a non-empty bounded canonical string", label) - } - for _, r := range id { - if r < 0x20 || r == 0x7f { - return fmt.Errorf("%s contains a control character", label) - } - } - return nil -} - -func decodePayload(label, encoded, digest string) ([]byte, error) { - if err := validateDigest(label+".payload_sha256", digest); err != nil { - return nil, err - } - data, err := base64.StdEncoding.Strict().DecodeString(encoded) - if err != nil || base64.StdEncoding.EncodeToString(data) != encoded { - return nil, fmt.Errorf("%s.payload_base64 is not canonical standard base64", label) - } - got := sha256.Sum256(data) - if hex.EncodeToString(got[:]) != digest { - return nil, fmt.Errorf("%s payload SHA-256 mismatch", label) - } - return data, nil -} - -func validateDigest(label, digest string) error { - if len(digest) != sha256.Size*2 || strings.ToLower(digest) != digest { - return fmt.Errorf("%s must be a lowercase SHA-256 hex digest", label) - } - decoded, err := hex.DecodeString(digest) - if err != nil || len(decoded) != sha256.Size { - return fmt.Errorf("%s must be a lowercase SHA-256 hex digest", label) - } - return nil -} - -func conflictingPath(state map[string]logicalFile, candidate, ignored string) string { - for existing := range state { - if existing == ignored { - continue - } - if strings.HasPrefix(candidate, existing+"/") || strings.HasPrefix(existing, candidate+"/") { - return existing - } - } - return "" -} - -func cloneBytes(value []byte) []byte { return append([]byte(nil), value...) } - -func projectedChunks(payloadBytes, chunkSize int) int { - if payloadBytes == 0 { - return 1 - } - return (payloadBytes + chunkSize - 1) / chunkSize -} diff --git a/nodeapi/block.go b/nodeapi/block.go new file mode 100644 index 0000000..0777484 --- /dev/null +++ b/nodeapi/block.go @@ -0,0 +1,21 @@ +package nodeapi + +import ( + cid "github.com/ipfs/go-cid" + mh "github.com/multiformats/go-multihash" +) + +// CIDForBlock computes the CIDv1 SHA-256 identity of immutable block bytes. +// Codec zero selects raw bytes. This helper does not write or accept a block; +// callers still validate the CID returned by the selected node service. +func CIDForBlock(block Block) (cid.Cid, error) { + codec := block.Codec + if codec == 0 { + codec = cid.Raw + } + hash, err := mh.Sum(block.Data, mh.SHA2_256, -1) + if err != nil { + return cid.Undef, err + } + return cid.NewCidV1(codec, hash), nil +} diff --git a/nodeapi/block_test.go b/nodeapi/block_test.go new file mode 100644 index 0000000..e1aab6d --- /dev/null +++ b/nodeapi/block_test.go @@ -0,0 +1,31 @@ +package nodeapi_test + +import ( + "encoding/hex" + "testing" + + "github.com/dewebprotocol/malt-client/nodeapi" + cid "github.com/ipfs/go-cid" + mh "github.com/multiformats/go-multihash" +) + +func TestCIDForBlockPreservesCodecAndSHA256Identity(t *testing.T) { + for _, codec := range []uint64{0, cid.Raw, cid.DagProtobuf, cid.DagCBOR} { + id, err := nodeapi.CIDForBlock(nodeapi.Block{Codec: codec, Data: []byte("abc")}) + if err != nil { + t.Fatal(err) + } + wantCodec := codec + if wantCodec == 0 { + wantCodec = cid.Raw + } + hash, err := mh.Decode(id.Hash()) + if err != nil { + t.Fatal(err) + } + if id.Version() != 1 || id.Type() != wantCodec || hash.Code != mh.SHA2_256 || + hex.EncodeToString(hash.Digest) != "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" { + t.Fatalf("codec %d: unexpected block identity %s", codec, id) + } + } +} diff --git a/tools/evaluation/README.md b/tools/evaluation/README.md deleted file mode 100644 index cbe0bd4..0000000 --- a/tools/evaluation/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# MALT runtime evaluation adapters - -This directory contains runtime-owned process adapters launched by the separate -[`malt-evaluation`](https://github.com/DeWebProtocol/malt-evaluation) -repository. Keeping an adapter beside the implementation lets it measure and -verify the exact runtime code at a pinned commit. - -This directory does not own benchmark suites, campaign plans, comparison -policy, result schemas, result interpretation, or provenance. Those remain in -`malt-evaluation`. The adapters are not supported user commands or a public Go -API, even though their executable names and JSON/JSONL contracts are pinned -campaign inputs. - -## Commands - -| Command | Runtime-side responsibility | -| --- | --- | -| `malt-eval-machine-descriptor-probe` | Reports bounded machine/runtime evidence consumed by evaluation setup. | -| `malt-eval-rq1-fixture-build` | Builds and verifies the runtime-owned RQ1 route fixture against a disposable Gateway. | -| `malt-eval-rq1-worker` | Measures locally verified selective-CAR and Direct-CAS read paths. | -| `malt-eval-rq2-fixture-build` | Builds and verifies shared KZG/IPA client-root fixtures. | -| `malt-eval-rq2-worker` | Runs the native client-root mutation adapter. | -| `malt-eval-rq2-browser-wasm` | Implements the Go/WASM mutation boundary used by the browser adapter. | -| `malt-eval-rq2-browser-worker` | Drives the browser/WASM process boundary and emits its measurement records. | -| `malt-eval-rq3-malt-worker` | Replays the portable RQ3 mutation stream through the MALT runtime/Gateway path. | -| `malt-eval-rq3-hash-worker` | Replays the RQ3 comparison stream through the runtime-owned hash-system adapter. | - -Build the adapters without placing them in the product command namespace: - -```bash -go build -buildvcs=false ./tools/evaluation/cmd/... -``` - -## Private support boundary - -Shared adapter code lives under `internal/evaluation`. In particular, -`internal/evaluation/gatewaytransport` owns the disposable-Gateway instance -token, bootstrap authorization, unchecked raw-CAS fetch, selective-CAR fetch, -and evaluation health projection. These capabilities are intentionally absent -from public package `transport`. - -Production packages and `cmd/malt` must not import `internal/evaluation`. -Architecture tests enforce that dependency direction and keep `cmd/` reserved -for the product binary. Code under this directory may use public runtime -packages to exercise real product behavior, but production code must never -depend on an evaluator adapter. diff --git a/tools/evaluation/cmd/malt-eval-local-files/main.go b/tools/evaluation/cmd/malt-eval-local-files/main.go deleted file mode 100644 index 82a44be..0000000 --- a/tools/evaluation/cmd/malt-eval-local-files/main.go +++ /dev/null @@ -1,339 +0,0 @@ -// malt-eval-local-files measures the real verified UnixFS facade over bounded -// in-memory authentication and payload capabilities. It is not a Gateway API. -package main - -import ( - "bufio" - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "os/signal" - "sort" - "strings" - - casmemory "github.com/dewebprotocol/malt-client/internal/cas/memory" - "github.com/dewebprotocol/malt-client/internal/strictjson" - "github.com/dewebprotocol/malt-client/unixfs" - "github.com/dewebprotocol/malt-core/auth/arcset/materializer/memory" - "github.com/dewebprotocol/malt-core/auth/commitment/ipa" - "github.com/dewebprotocol/malt-core/auth/commitment/kzg" - "github.com/dewebprotocol/malt-core/engine" - "github.com/dewebprotocol/malt-core/maltcid" - "github.com/dewebprotocol/malt-core/protocol" - "github.com/dewebprotocol/malt-core/sdk/authentication" - cid "github.com/ipfs/go-cid" -) - -const requestSchema = "malt-local-file-request/v1" -const responseSchema = "malt-local-file-response/v1" -const maxLine = 64 << 20 - -type request struct { - Schema string `json:"schema"` - ID string `json:"id"` - Operation string `json:"operation"` - Backend string `json:"backend,omitempty"` - ChunkBytes int `json:"chunk_bytes,omitempty"` - Files map[string][]byte `json:"files,omitempty"` - Root string `json:"root,omitempty"` - Path string `json:"path,omitempty"` - Offset *uint64 `json:"offset,omitempty"` - Length *uint64 `json:"length,omitempty"` -} - -type response struct { - Schema string `json:"schema"` - ID string `json:"id"` - Root string `json:"root"` - Verified bool `json:"verified"` - Absent bool `json:"absent"` - BodySHA256 string `json:"body_sha256"` - ReturnedBytes uint64 `json:"returned_bytes"` - ProofBytes uint64 `json:"proof_bytes"` - CASBytes uint64 `json:"cas_bytes"` - Calls uint64 `json:"calls"` - Error string `json:"error"` -} - -type sourceStore struct { - *casmemory.Store - e *engine.Engine - nodes *memory.Nodes - candidates map[string]protocol.AuthenticationCandidate - proofBytes, casBytes, calls uint64 -} - -func (s *sourceStore) Get(ctx context.Context, key cid.Cid) ([]byte, error) { - s.calls++ - body, err := s.Store.Get(ctx, key) - s.casBytes += uint64(len(body)) - return body, err -} -func (s *sourceStore) Authenticate(ctx context.Context, q protocol.AuthenticationRequest) (*protocol.AuthenticationResult, error) { - s.calls++ - result, err := authentication.Execute(ctx, s.e, q, s.nodes) - if err != nil { - return nil, err - } - body, err := json.Marshal(result) - if err != nil { - return nil, err - } - s.proofBytes += uint64(len(body)) - // The verified client consumes decoded untrusted evidence, not the server's - // internal object or a pre-supplied final CID. - var decoded protocol.AuthenticationResult - if err := strictjson.Decode(body, &decoded); err != nil { - return nil, err - } - return &decoded, nil -} -func (s *sourceStore) MaterializeAuthentication(ctx context.Context, candidate protocol.AuthenticationCandidate) (cid.Cid, error) { - if err := authentication.Materialize(ctx, s.e, candidate, s.nodes); err != nil { - return cid.Undef, err - } - root, err := cid.Decode(candidate.Root) - if err == nil { - s.candidates[root.KeyString()] = candidate - } - return root, err -} -func (s *sourceStore) AuthenticationCandidate(ctx context.Context, root cid.Cid) (*protocol.AuthenticationCandidate, error) { - candidate, ok := s.candidates[root.KeyString()] - if !ok { - return nil, fmt.Errorf("source candidate absent") - } - out, err := authentication.Export(ctx, s.e, root, candidate.State, s.nodes) - return &out, err -} - -type session struct { - root cid.Cid - store *sourceStore - reader unixfs.Reader -} - -func initialize(ctx context.Context, q request) (*session, error) { - if q.Files == nil || len(q.Files) > 100_000 || q.ChunkBytes < 1 || q.ChunkBytes > 4<<20 { - return nil, fmt.Errorf("explicit files and chunk size within 1..4MiB required") - } - names := make([]string, 0, len(q.Files)) - total := uint64(0) - for name, body := range q.Files { - parts, err := unixfs.ParseCanonicalStagedPath(name) - if err != nil || len(parts) == 0 || strings.Join(parts, "/") != name { - return nil, fmt.Errorf("invalid canonical source path %q", name) - } - for i := 1; i < len(parts); i++ { - if _, exists := q.Files[strings.Join(parts[:i], "/")]; exists { - return nil, fmt.Errorf("file/directory source collision") - } - } - total += uint64(len(body)) - if total > 32<<20 { - return nil, fmt.Errorf("source payload exceeds 32MiB") - } - names = append(names, name) - } - registry := engine.NewRegistry() - profile := maltcid.KZG4096 - if q.Backend == "kzg" { - scheme, err := kzg.NewScheme() - if err != nil { - return nil, err - } - if err = registry.Register(scheme); err != nil { - return nil, err - } - } else if q.Backend == "ipa" { - profile = maltcid.IPA256 - scheme, err := ipa.NewCommitterScheme(ipa.ProfileCompact) - if err != nil { - return nil, err - } - if err = registry.Register(scheme); err != nil { - return nil, err - } - } else { - return nil, fmt.Errorf("unknown backend") - } - store := &sourceStore{Store: casmemory.New(), e: engine.New(registry), nodes: memory.NewNodes(), candidates: map[string]protocol.AuthenticationCandidate{}} - adapter, err := unixfs.NewAuthenticationAdapter(unixfs.LayoutRootedV1, store, store.e, profile) - if err != nil { - return nil, err - } - tree := unixfs.NewStagedDirectory() - tree.Changed = true - sort.Strings(names) - for _, name := range names { - body := q.Files[name] - target, _, err := unixfs.MaterializeStagedFilePayload(ctx, store, adapter, bytes.NewReader(body), int64(len(body)), q.ChunkBytes) - if err != nil { - return nil, err - } - if err := unixfs.SetStagedFile(tree, name, target); err != nil { - return nil, err - } - } - layout, err := unixfs.NewLayout(unixfs.LayoutRootedV1) - if err != nil { - return nil, err - } - result, err := layout.Materialize(ctx, adapter, store, tree) - if err != nil { - return nil, err - } - reader, err := unixfs.NewReader(unixfs.ReaderOptions{Layout: unixfs.LayoutRootedV1, Remote: store, Blocks: store, Verifier: store.e}) - if err != nil { - return nil, err - } - // Independently read every fixture object through the client before timing. - for _, name := range names { - read, err := reader.ReadFile(ctx, result.Key, name) - if err != nil { - return nil, fmt.Errorf("source readback failed for %s: %w", name, err) - } - if read == nil || !bytes.Equal(read.Body, q.Files[name]) { - return nil, fmt.Errorf("source readback bytes disagree for %s", name) - } - } - return &session{root: result.Key, store: store, reader: reader}, nil -} - -func (s *session) execute(ctx context.Context, q request) response { - r := response{Schema: responseSchema, ID: q.ID, Root: s.root.String()} - s.store.proofBytes, s.store.casBytes, s.store.calls = 0, 0, 0 - if q.Root != s.root.String() { - r.Error = "query changed caller-selected root" - return r - } - parts, err := unixfs.ParseCanonicalStagedPath(q.Path) - if err != nil || len(parts) == 0 || strings.Join(parts, "/") != q.Path { - r.Error = "invalid original path" - return r - } - var read *unixfs.ReadResult - if q.Operation == "read" { - read, err = s.reader.ReadFile(ctx, s.root, q.Path) - } else { - read, err = s.reader.ReadFileRange(ctx, s.root, q.Path, *q.Offset, *q.Length) - } - r.ProofBytes, r.CASBytes, r.Calls = s.store.proofBytes, s.store.casBytes, s.store.calls - if errors.Is(err, unixfs.ErrNotFound) { - r.Verified, r.Absent = true, true - return r - } - if err != nil { - r.Error = err.Error() - return r - } - if read == nil { - r.Error = "missing verified read result" - return r - } - digest := sha256.Sum256(read.Body) - r.BodySHA256, r.ReturnedBytes, r.Verified = hex.EncodeToString(digest[:]), uint64(len(read.Body)), true - return r -} - -func decode(data []byte) (request, error) { - var q request - if err := strictjson.Decode(data, &q); err != nil { - return q, err - } - var fields map[string]json.RawMessage - if err := json.Unmarshal(data, &fields); err != nil { - return q, err - } - keys := []string{"schema", "id", "operation"} - switch q.Operation { - case "init": - keys = append(keys, "backend", "chunk_bytes", "files") - case "read": - keys = append(keys, "root", "path") - case "range": - keys = append(keys, "root", "path", "offset", "length") - case "close": - default: - return q, fmt.Errorf("unknown operation") - } - if q.Schema != requestSchema || q.ID == "" || len(q.ID) > 128 || len(fields) != len(keys) { - return q, fmt.Errorf("invalid request or ignored operation fields") - } - for _, key := range keys { - if body, exists := fields[key]; !exists || bytes.Equal(body, []byte("null")) { - return q, fmt.Errorf("explicit nonnull %s required", key) - } - } - if q.Operation == "range" && (q.Length == nil || *q.Length == 0) { - return q, fmt.Errorf("positive range length required") - } - return q, nil -} - -func serve(ctx context.Context, input io.Reader, output io.Writer) error { - scanner := bufio.NewScanner(input) - scanner.Buffer(make([]byte, 64<<10), maxLine) - encoder := json.NewEncoder(output) - var state *session - ids := map[string]bool{} - closed := false - for scanner.Scan() { - q, err := decode(scanner.Bytes()) - if err != nil { - return err - } - if closed || ids[q.ID] || len(ids) >= 1_000_000 { - return fmt.Errorf("duplicate, excessive or terminal request") - } - ids[q.ID] = true - r := response{Schema: responseSchema, ID: q.ID} - if q.Operation == "init" { - if state != nil { - return fmt.Errorf("fixture already initialized") - } - state, err = initialize(ctx, q) - if err != nil { - return err - } - r.Root, r.Verified = state.root.String(), true - } else if q.Operation == "close" { - closed = true - r.Verified = true - } else { - if state == nil { - return fmt.Errorf("fixture not initialized") - } - r = state.execute(ctx, q) - } - if err := encoder.Encode(r); err != nil { - return err - } - } - if err := scanner.Err(); err != nil { - return err - } - if !closed { - return fmt.Errorf("missing explicit close") - } - return nil -} - -func main() { - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) - defer stop() - if len(os.Args) > 1 { - fmt.Fprintln(os.Stderr, "local file worker accepts no arguments") - os.Exit(1) - } - if err := serve(ctx, os.Stdin, os.Stdout); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} diff --git a/tools/evaluation/cmd/malt-eval-local-files/main_test.go b/tools/evaluation/cmd/malt-eval-local-files/main_test.go deleted file mode 100644 index b3b67e6..0000000 --- a/tools/evaluation/cmd/malt-eval-local-files/main_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package main - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "strings" - "testing" - - "github.com/dewebprotocol/malt-client/unixfs" - cid "github.com/ipfs/go-cid" -) - -func digest(body []byte) string { h := sha256.Sum256(body); return hex.EncodeToString(h[:]) } - -func TestLocalFilesReadEveryPayloadChunkRangeAndAbsence(t *testing.T) { - for _, backend := range []string{"kzg", "ipa"} { - t.Run(backend, func(t *testing.T) { - files := map[string][]byte{"dir/a": bytes.Repeat([]byte("abc"), 25), "dir/deep/b": []byte("real side payload"), "empty": {}} - s, err := initialize(t.Context(), request{Backend: backend, ChunkBytes: 16, Files: files}) - if err != nil { - t.Fatal(err) - } - for path, body := range files { - got := s.execute(t.Context(), request{ID: path, Operation: "read", Root: s.root.String(), Path: path}) - if !got.Verified || got.Absent || got.Error != "" || got.BodySHA256 != digest(body) || got.ReturnedBytes != uint64(len(body)) || got.ProofBytes == 0 || got.Calls == 0 { - t.Fatalf("read %s: %+v", path, got) - } - } - for _, path := range []string{"missing", "dir/deep/missing"} { - got := s.execute(t.Context(), request{ID: path, Operation: "read", Root: s.root.String(), Path: path}) - if !got.Verified || !got.Absent || got.Error != "" || got.ReturnedBytes != 0 || got.ProofBytes == 0 { - t.Fatalf("absence: %+v", got) - } - } - for _, limits := range [][2]uint64{{0, 1}, {15, 35}, {70, 100}} { - offset, length := limits[0], limits[1] - got := s.execute(t.Context(), request{ID: "range", Operation: "range", Root: s.root.String(), Path: "dir/a", Offset: &offset, Length: &length}) - end := min(offset+length, uint64(len(files["dir/a"]))) - if !got.Verified || got.Absent || got.Error != "" || got.BodySHA256 != digest(files["dir/a"][offset:end]) || got.ReturnedBytes != end-offset { - t.Fatalf("range: %+v", got) - } - } - got := s.execute(t.Context(), request{ID: "wrong-root", Operation: "read", Root: "bafkqaaa", Path: "dir/a"}) - if got.Verified || got.Error == "" || got.Calls != 0 { - t.Fatal("changed selected root accepted", got) - } - }) - } -} - -type corruptBlocks struct{ *sourceStore } - -func (c corruptBlocks) Get(ctx context.Context, key cid.Cid) ([]byte, error) { - body, err := c.sourceStore.Get(ctx, key) - if len(body) > 0 { - body[0] ^= 0xff - } - return body, err -} - -func TestLocalFileClientRejectsWrongPayloadBytes(t *testing.T) { - s, err := initialize(t.Context(), request{Backend: "ipa", ChunkBytes: 8, Files: map[string][]byte{"a": bytes.Repeat([]byte("x"), 25)}}) - if err != nil { - t.Fatal(err) - } - s.reader, err = unixfs.NewReader(unixfs.ReaderOptions{Layout: unixfs.LayoutRootedV1, Remote: s.store, Blocks: corruptBlocks{s.store}, Verifier: s.store.e}) - if err != nil { - t.Fatal(err) - } - got := s.execute(t.Context(), request{ID: "corrupt", Operation: "read", Root: s.root.String(), Path: "a"}) - if got.Verified || got.Absent || got.Error == "" { - t.Fatal("corrupted CAS bytes accepted or misclassified absent", got) - } -} - -func TestLocalFileProtocolRejectsIgnoredFieldsAndPartialLifecycle(t *testing.T) { - for _, body := range []string{ - `{"schema":"malt-local-file-request/v1","id":"x","operation":"close","backend":"ipa"}`, - `{"schema":"malt-local-file-request/v1","id":"x","operation":"close","id":"y"}`, - `{"schema":"malt-local-file-request/v1","id":"x","operation":"range","root":"r","path":"a","offset":0,"length":null}`, - `{"schema":"malt-local-file-request/v1","id":"x","operation":"init","backend":"ipa","chunk_bytes":8,"files":{"a":"eA==","a":"eQ=="}}`, - } { - if _, err := decode([]byte(body)); err == nil { - t.Fatal("invalid protocol accepted", body) - } - } - if _, err := initialize(t.Context(), request{Backend: "ipa", ChunkBytes: 8, Files: map[string][]byte{"a": {}, "a/b": {}}}); err == nil { - t.Fatal("source file/directory collision accepted") - } - var input, output bytes.Buffer - enc := json.NewEncoder(&input) - enc.Encode(request{Schema: requestSchema, ID: "init", Operation: "init", Backend: "ipa", ChunkBytes: 8, Files: map[string][]byte{"a": []byte("content")}}) - if err := serve(t.Context(), &input, &output); err == nil || !strings.Contains(err.Error(), "close") { - t.Fatal("incomplete lifecycle accepted", err) - } -} diff --git a/tools/evaluation/cmd/malt-eval-machine-descriptor-probe/main.go b/tools/evaluation/cmd/malt-eval-machine-descriptor-probe/main.go deleted file mode 100644 index 5bdc3a3..0000000 --- a/tools/evaluation/cmd/malt-eval-machine-descriptor-probe/main.go +++ /dev/null @@ -1,47 +0,0 @@ -// Command malt-eval-machine-descriptor-probe emits a descriptor template from -// the exact host probe used by paper evaluator workers. The maintainer supplies -// the campaign ID and classification evidence; the resulting file is then -// pinned by SHA-256 and byte length in E0/campaign registration. -package main - -import ( - "encoding/json" - "flag" - "fmt" - "os" - "strings" - - "github.com/dewebprotocol/malt-client/internal/evaluation/machine" -) - -func main() { - if err := run(os.Args[1:]); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func run(args []string) error { - flags := flag.NewFlagSet("malt-eval-machine-descriptor-probe", flag.ContinueOnError) - id := flags.String("id", "", "campaign machine descriptor ID") - classification := flags.String("classification", "", "general-purpose or low-power-arm") - evidence := flags.String("classification-evidence-source", "", "registered vendor/product evidence source") - if err := flags.Parse(args); err != nil { - return err - } - if flags.NArg() != 0 { - return fmt.Errorf("unexpected positional arguments") - } - identity, err := machine.Probe() - if err != nil { - return err - } - descriptor, err := machine.NewDescriptor(strings.TrimSpace(*id), strings.TrimSpace(*classification), strings.TrimSpace(*evidence), identity) - if err != nil { - return err - } - encoder := json.NewEncoder(os.Stdout) - encoder.SetEscapeHTML(false) - encoder.SetIndent("", " ") - return encoder.Encode(descriptor) -} diff --git a/tools/evaluation/cmd/malt-eval-replay-worker/main.go b/tools/evaluation/cmd/malt-eval-replay-worker/main.go deleted file mode 100644 index 2a76c5a..0000000 --- a/tools/evaluation/cmd/malt-eval-replay-worker/main.go +++ /dev/null @@ -1,199 +0,0 @@ -// Command malt-eval-replay-worker exposes actual incremental UnixFS writes. -// Its protocol is deliberately separate from both legacy RQ3 and the next -// protocol's latency executor. It makes no crash-durability claim. -package main - -import ( - "bufio" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "os/signal" - "reflect" - - "github.com/dewebprotocol/malt-client/internal/evaluation/rq3baseline" - "github.com/dewebprotocol/malt-client/internal/strictjson" -) - -const requestSchema = "malt-replay-worker-request/v2" -const responseSchema = "malt-replay-worker-response/v2" - -type request struct { - Schema string `json:"schema_version"` - ID string `json:"request_id"` - Operation string `json:"operation"` - Profile string `json:"profile"` - Run *rq3baseline.RunSpec `json:"run,omitempty"` -} - -type response struct { - Schema string `json:"schema_version"` - ID string `json:"request_id"` - OK bool `json:"ok"` - Error string `json:"error,omitempty"` - Profiles []string `json:"profiles,omitempty"` - Records []rq3baseline.CommitRecord `json:"records,omitempty"` - ObjectRoles [][]string `json:"object_roles,omitempty"` - Complete bool `json:"complete"` - ReadbackVerified bool `json:"readback_verified"` - CommitListSHA256 string `json:"commit_list_sha256,omitempty"` - Applied uint32 `json:"applied"` - RetainedObjects int64 `json:"retained_objects,string"` - RetainedBytes int64 `json:"retained_bytes,string"` - StorageBackend string `json:"storage_backend"` - Storage *rq3baseline.ReplayStorage `json:"storage,omitempty"` -} - -func main() { - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) - defer stop() - if len(os.Args) > 1 { - fmt.Fprintln(os.Stderr, "replay worker accepts no arguments") - os.Exit(1) - } - if err := run(ctx, os.Stdin, os.Stdout); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func run(ctx context.Context, input io.Reader, output io.Writer) (returnErr error) { - scanner := bufio.NewScanner(input) - scanner.Buffer(make([]byte, 64<<10), rq3baseline.MaximumJSONLRecordBytes) - encoder := json.NewEncoder(output) - var session *rq3baseline.StreamSession - defer func() { - if session != nil { - returnErr = errors.Join(returnErr, session.Close()) - } - }() - var profile string - var layout rq3baseline.LayoutSpec - var applied uint32 - capability, finished := false, false - for scanner.Scan() { - if err := ctx.Err(); err != nil { - return err - } - var req request - if !json.Valid(scanner.Bytes()) { - return fmt.Errorf("request must contain one JSON value") - } - if err := strictjson.Decode(scanner.Bytes(), &req); err != nil { - return err - } - if req.Schema != requestSchema || req.ID == "" || finished { - return fmt.Errorf("invalid replay request envelope or terminal stream") - } - res := response{Schema: responseSchema, ID: req.ID, OK: true, StorageBackend: rq3baseline.ReplayStorageBackend} - var err error - switch req.Operation { - case "capabilities": - if capability || session != nil || req.Run != nil || req.Profile != "" { - return fmt.Errorf("capabilities must be first") - } - capability = true - res.Profiles = []string{"merkledag-nested", "hamt-nested", "hamt-flat"} - case "start": - if !capability || session != nil || req.Run == nil { - return fmt.Errorf("invalid stream start") - } - profile, layout = req.Profile, req.Run.Layout - var record rq3baseline.CommitRecord - switch profile { - case "merkledag-nested": - if req.Run.System != rq3baseline.SystemMerkleDAGUnixFS { - return fmt.Errorf("Merkle DAG profile mismatch") - } - session, record, err = rq3baseline.StartReplayStream(ctx, *req.Run, false) - case "hamt-nested": - if req.Run.System != rq3baseline.SystemHAMTUnixFS { - return fmt.Errorf("HAMT profile mismatch") - } - session, record, err = rq3baseline.StartReplayStream(ctx, *req.Run, false) - case "hamt-flat": - session, record, err = rq3baseline.StartReplayStream(ctx, *req.Run, true) - default: - return fmt.Errorf("unsupported profile %q", profile) - } - if err == nil { - paths := make([]string, len(req.Run.Snapshot.Files)) - for i, file := range req.Run.Snapshot.Files { - paths[i] = file.Path - } - err = session.VerifyPaths(ctx, paths) - } - if err == nil { - res.Records = []rq3baseline.CommitRecord{record} - applied = 1 - } - case "chunk": - if session == nil || req.Run == nil || req.Profile != profile || !reflect.DeepEqual(layout, req.Run.Layout) || req.Run.Snapshot.CommitID != "" || req.Run.Snapshot.Files != nil { - return fmt.Errorf("chunk does not bind active stream") - } - if req.Run.System != rq3baseline.SystemMerkleDAGUnixFS && req.Run.System != rq3baseline.SystemHAMTUnixFS { - return fmt.Errorf("chunk system mismatch") - } - if (profile == "merkledag-nested") != (req.Run.System == rq3baseline.SystemMerkleDAGUnixFS) { - return fmt.Errorf("chunk system mismatch") - } - res.Records, err = session.ApplyChunkWithReadback(ctx, req.Run.Commits) - if err == nil { - applied += uint32(len(res.Records)) - } - case "finish": - if session == nil || req.Run != nil || req.Profile != profile { - return fmt.Errorf("invalid stream finish") - } - res.CommitListSHA256, err = session.CommitListSHA256() - if err == nil { - err = session.VerifyAll(ctx) - } - if err == nil { - res.Storage, err = session.FinishReplayStorage() - if err == nil { - res.RetainedObjects, res.RetainedBytes = res.Storage.Objects, res.Storage.BodyBytes - } - } - if err == nil { - err = session.Close() - } - if err == nil { - session = nil - finished = true - res.Complete = true - } - default: - return fmt.Errorf("unsupported replay operation %q", req.Operation) - } - res.Applied = applied - for _, record := range res.Records { - roles := make([]string, len(record.CAS.Events)) - for i, event := range record.CAS.Events { - roles[i] = event.ObjectRole - } - res.ObjectRoles = append(res.ObjectRoles, roles) - } - res.ReadbackVerified = applied > 0 && err == nil - if err != nil { - res.OK = false - res.Error = err.Error() - } - if encodeErr := encoder.Encode(res); encodeErr != nil { - return encodeErr - } - if err != nil { - return err - } - } - if err := scanner.Err(); err != nil { - return err - } - if !finished { - return fmt.Errorf("incomplete replay stream") - } - return nil -} diff --git a/tools/evaluation/cmd/malt-eval-replay-worker/main_test.go b/tools/evaluation/cmd/malt-eval-replay-worker/main_test.go deleted file mode 100644 index 094d219..0000000 --- a/tools/evaluation/cmd/malt-eval-replay-worker/main_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package main - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "strings" - "testing" - - "github.com/dewebprotocol/malt-client/internal/evaluation/rq3baseline" -) - -func TestReplayProtocolVerifiesModesAndRetainsNoOpCommits(t *testing.T) { - data := []byte("file") - hash := sha256.Sum256(data) - digest := hex.EncodeToString(hash[:]) - mode, newMode, raw := uint32(0o644), uint32(0o755), true - layout := rq3baseline.LayoutSpec{Model: "unixfs", FileLayout: "balanced", DirectoryLayout: "hamt", Chunking: rq3baseline.ChunkingSpec{Algorithm: "fixed", SizeBytes: 262144}, HAMTFanout: 256, RawFileLeaf: &raw} - initial := rq3baseline.RunSpec{System: rq3baseline.SystemHAMTUnixFS, Layout: layout, Snapshot: rq3baseline.Snapshot{CommitID: "initial", Files: []rq3baseline.FrozenFile{{Path: "nested/file", FileKind: "regular", PayloadBase64: base64.StdEncoding.EncodeToString(data), PayloadSHA256: digest, Mode: &mode}}}, Commits: []rq3baseline.Commit{}} - chunk := rq3baseline.RunSpec{System: rq3baseline.SystemHAMTUnixFS, Layout: layout, Commits: []rq3baseline.Commit{{CommitID: "mode", Mutations: []rq3baseline.Mutation{{Kind: "mode-change", Path: "nested/file", FileKind: "regular", ExpectedOldSHA256: digest, ExpectedOldMode: &mode, Mode: &newMode}}}, {CommitID: "no-op", Mutations: []rq3baseline.Mutation{}}}} - var input, output bytes.Buffer - encoder := json.NewEncoder(&input) - for _, req := range []request{{Schema: requestSchema, ID: "1", Operation: "capabilities"}, {Schema: requestSchema, ID: "2", Operation: "start", Profile: "hamt-flat", Run: &initial}, {Schema: requestSchema, ID: "3", Operation: "chunk", Profile: "hamt-flat", Run: &chunk}, {Schema: requestSchema, ID: "4", Operation: "finish", Profile: "hamt-flat"}} { - if err := encoder.Encode(req); err != nil { - t.Fatal(err) - } - } - if err := run(context.Background(), &input, &output); err != nil { - t.Fatal(err) - } - decoder := json.NewDecoder(&output) - var replies [4]response - for i := range replies { - if err := decoder.Decode(&replies[i]); err != nil { - t.Fatal(err) - } - if !replies[i].OK { - t.Fatal(replies[i].Error) - } - } - if !replies[3].Complete || !replies[3].ReadbackVerified || replies[3].Applied != 3 || replies[3].RetainedObjects == 0 { - t.Fatal("incomplete verified history") - } - storage := replies[3].Storage - if storage == nil || replies[3].StorageBackend != rq3baseline.ReplayStorageBackend || !storage.Reconciled || storage.Objects != replies[3].RetainedObjects || storage.BodyBytes != replies[3].RetainedBytes || storage.IndexWriterComplete { - t.Fatal("terminal response omitted or overclaimed backend observation") - } - if replies[1].Records[0].Root == replies[2].Records[0].Root || replies[2].Records[0].Root != replies[2].Records[1].Root { - t.Fatal("mode change or no-op root semantics lost") - } - if replies[2].Records[0].AdapterPayloadInputBytes != 0 || len(replies[2].Records[1].CAS.Events) != 0 || len(replies[2].ObjectRoles) != 2 { - t.Fatal("zero-source commit or ownership accounting lost") - } -} - -func TestReplayRejectsAmbiguousOrIncompleteInput(t *testing.T) { - for _, input := range []string{ - `{"schema_version":"malt-replay-worker-request/v2","request_id":"1","operation":"capabilities","profile":""} {}`, - `{"schema_version":"malt-replay-worker-request/v2","request_id":"1","request_id":"2","operation":"capabilities","profile":""}`, - `{"schema_version":"malt-replay-worker-request/v2","request_id":"1","operation":"capabilities","profile":""}`, - `{"schema_version":"malt-replay-worker-request/v1","request_id":"1","operation":"capabilities","profile":""}`, - } { - var output bytes.Buffer - if err := run(context.Background(), strings.NewReader(input+"\n"), &output); err == nil { - t.Fatal("ambiguous or unfinished stream accepted") - } - } -} - -func TestReplayEmptyInitialTreeDeleteAllAndResume(t *testing.T) { - for _, profile := range []string{"merkledag-nested", "hamt-nested", "hamt-flat"} { - t.Run(profile, func(t *testing.T) { - raw, mode := true, uint32(0o644) - layout := rq3baseline.LayoutSpec{Model: "unixfs", FileLayout: "balanced", DirectoryLayout: "hamt", Chunking: rq3baseline.ChunkingSpec{Algorithm: "fixed", SizeBytes: 262144}, HAMTFanout: 256, RawFileLeaf: &raw} - system := rq3baseline.SystemHAMTUnixFS - if profile == "merkledag-nested" { - system, layout.DirectoryLayout, layout.HAMTFanout = rq3baseline.SystemMerkleDAGUnixFS, "basic", 0 - } - initial := rq3baseline.RunSpec{System: system, Layout: layout, Snapshot: rq3baseline.Snapshot{CommitID: "empty", Files: []rq3baseline.FrozenFile{}}, Commits: []rq3baseline.Commit{}} - data := []byte("from empty") - digest := sha256.Sum256(data) - hash := hex.EncodeToString(digest[:]) - put := rq3baseline.Mutation{Kind: "insert", Path: "nested/file", FileKind: "regular", Mode: &mode, PayloadBase64: base64.StdEncoding.EncodeToString(data), PayloadSHA256: hash} - del := rq3baseline.Mutation{Kind: "delete", Path: "nested/file", FileKind: "regular", ExpectedOldMode: &mode, ExpectedOldSHA256: hash} - chunk := rq3baseline.RunSpec{System: system, Layout: layout, Commits: []rq3baseline.Commit{{CommitID: "put", Mutations: []rq3baseline.Mutation{put}}, {CommitID: "delete", Mutations: []rq3baseline.Mutation{del}}, {CommitID: "no-op", Mutations: []rq3baseline.Mutation{}}, {CommitID: "resume", Mutations: []rq3baseline.Mutation{put}}, {CommitID: "empty-again", Mutations: []rq3baseline.Mutation{del}}}} - var input, output bytes.Buffer - encoder := json.NewEncoder(&input) - for _, req := range []request{{Schema: requestSchema, ID: "1", Operation: "capabilities"}, {Schema: requestSchema, ID: "2", Operation: "start", Profile: profile, Run: &initial}, {Schema: requestSchema, ID: "3", Operation: "chunk", Profile: profile, Run: &chunk}, {Schema: requestSchema, ID: "4", Operation: "finish", Profile: profile}} { - if err := encoder.Encode(req); err != nil { - t.Fatal(err) - } - } - if err := run(context.Background(), &input, &output); err != nil { - t.Fatal(err) - } - decoder := json.NewDecoder(&output) - var replies [4]response - for i := range replies { - if err := decoder.Decode(&replies[i]); err != nil { - t.Fatal(err) - } - } - if !replies[3].Complete || !replies[3].ReadbackVerified || replies[3].Applied != 6 { - t.Fatal("empty history lost coverage or readback") - } - start := replies[1].Records[0] - if start.AdapterPayloadInputBytes != 0 || start.Root == "" || start.CAS.Total.NewlyPersistedBytes == 0 { - t.Fatal("empty root not materialized and accounted") - } - for _, index := range []int{1, 2, 4} { - if replies[2].Records[index].Root != start.Root { - t.Fatal("canonical empty root was not restored") - } - } - if len(replies[2].Records[2].CAS.Events) != 0 { - t.Fatal("empty no-op rewrote objects") - } - }) - } -} diff --git a/tools/evaluation/cmd/malt-eval-rooted-file-trace/main.go b/tools/evaluation/cmd/malt-eval-rooted-file-trace/main.go deleted file mode 100644 index 6e202ca..0000000 --- a/tools/evaluation/cmd/malt-eval-rooted-file-trace/main.go +++ /dev/null @@ -1,231 +0,0 @@ -// malt-eval-rooted-file-trace compiles bounded file snapshots through the real -// rooted-v1 application schema. It exports authentication-only evaluation data. -package main - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "flag" - "fmt" - "io" - "os" - "sort" - "strings" - - "github.com/dewebprotocol/malt-client/unixfs" - "github.com/dewebprotocol/malt-core/auth/commitment/ipa" - "github.com/dewebprotocol/malt-core/auth/commitment/kzg" - "github.com/dewebprotocol/malt-core/engine" - "github.com/dewebprotocol/malt-core/maltcid" - "github.com/dewebprotocol/malt-core/protocol" - "github.com/dewebprotocol/malt-core/sdk/authentication" - cid "github.com/ipfs/go-cid" - mh "github.com/multiformats/go-multihash" -) - -type version struct { - Files map[string][]byte `json:"files"` - Queries []string `json:"queries"` -} -type trace struct { - Schema string `json:"schema"` - Integration string `json:"integration"` - SourceSHA256 string `json:"source_sha256"` - Candidates []protocol.AuthenticationCandidate `json:"candidates"` - Queries []protocol.AuthenticationRequest `json:"queries"` -} -type sink struct { - e *engine.Engine - candidates map[string]protocol.AuthenticationCandidate - ordered []protocol.AuthenticationCandidate -} - -func (s *sink) MaterializeAuthentication(ctx context.Context, c protocol.AuthenticationCandidate) (cid.Cid, error) { - if err := authentication.ValidateCandidate(ctx, s.e, c); err != nil { - return cid.Undef, err - } - root, err := cid.Decode(c.Root) - if err != nil { - return cid.Undef, err - } - if _, ok := s.candidates[c.Root]; !ok { - s.ordered = append(s.ordered, c) - s.candidates[c.Root] = c - } - return root, nil -} -func (s *sink) AuthenticationCandidate(_ context.Context, root cid.Cid) (*protocol.AuthenticationCandidate, error) { - c, ok := s.candidates[root.String()] - if !ok { - return nil, fmt.Errorf("base candidate missing") - } - return &c, nil -} -func (s *sink) Put(ctx context.Context, b []byte) (cid.Cid, error) { - return s.PutWithCodec(ctx, b, cid.Raw) -} -func (*sink) PutWithCodec(_ context.Context, b []byte, codec uint64) (cid.Cid, error) { - return (cid.Prefix{Version: 1, Codec: codec, MhType: mh.SHA2_256, MhLength: -1}).Sum(b) -} -func compile(ctx context.Context, data []byte, backend string, chunk int) (trace, error) { - out := trace{Schema: "malt.rooted-trace/v1", Integration: "malt.unixfs/rooted-v1/authentication-only"} - if chunk <= 0 || chunk > 64<<20 { - return out, fmt.Errorf("chunk size outside 1..64MiB") - } - var versions []version - d := json.NewDecoder(bytes.NewReader(data)) - d.DisallowUnknownFields() - if err := d.Decode(&versions); err != nil { - return out, err - } - if d.Decode(new(any)) != io.EOF { - return out, fmt.Errorf("trailing data") - } - if len(versions) == 0 || len(versions) > 1000 { - return out, fmt.Errorf("snapshot count outside 1..1000") - } - digest := sha256.Sum256(data) - out.SourceSHA256 = hex.EncodeToString(digest[:]) - profiles := engine.NewRegistry() - profile := maltcid.KZG4096 - switch backend { - case "kzg": - s, err := kzg.NewScheme() - if err != nil { - return out, err - } - if err = profiles.Register(s); err != nil { - return out, err - } - case "ipa": - profile = maltcid.IPA256 - s, err := ipa.NewCommitterScheme(ipa.ProfileCompact) - if err != nil { - return out, err - } - if err = profiles.Register(s); err != nil { - return out, err - } - default: - return out, fmt.Errorf("unknown backend") - } - store := &sink{e: engine.New(profiles), candidates: map[string]protocol.AuthenticationCandidate{}} - adapter, err := unixfs.NewAuthenticationAdapter(unixfs.LayoutRootedV1, store, store.e, profile) - if err != nil { - return out, err - } - layout, _ := unixfs.NewLayout(unixfs.LayoutRootedV1) - previous := map[string]cid.Cid{} - for _, v := range versions { - tree := unixfs.NewStagedDirectory() - tree.Changed = true - names := make([]string, 0, len(v.Files)) - for name := range v.Files { - names = append(names, name) - } - sort.Strings(names) - for _, name := range names { - parts, err := unixfs.ParseCanonicalStagedPath(name) - if err != nil || len(parts) == 0 { - return out, fmt.Errorf("invalid file path %q", name) - } - for j := 1; j < len(parts); j++ { - if _, ok := v.Files[strings.Join(parts[:j], "/")]; ok { - return out, fmt.Errorf("file/directory collision: %s", name) - } - } - body := v.Files[name] - target, _, err := unixfs.MaterializeStagedFilePayload(ctx, store, adapter, bytes.NewReader(body), int64(len(body)), chunk) - if err != nil { - return out, err - } - if err = unixfs.SetStagedFile(tree, name, target); err != nil { - return out, err - } - } - var bind func(*unixfs.StagedNode, string) - bind = func(n *unixfs.StagedNode, path string) { - if n.Kind != unixfs.StagedKindDirectory { - return - } - n.Key = previous[path] - n.Changed = true - for name, child := range n.Children { - p := name - if path != "" { - p = path + "/" + name - } - bind(child, p) - } - } - bind(tree, "") - result, err := layout.Materialize(ctx, adapter, store, tree) - if err != nil { - return out, err - } - previous = map[string]cid.Cid{} - var remember func(*unixfs.StagedNode, string) - remember = func(n *unixfs.StagedNode, path string) { - if n.Kind != unixfs.StagedKindDirectory { - return - } - previous[path] = n.Key - for name, child := range n.Children { - p := name - if path != "" { - p = path + "/" + name - } - remember(child, p) - } - } - remember(tree, "") - for _, path := range v.Queries { - parts, err := unixfs.ParseCanonicalStagedPath(path) - if err != nil { - return out, err - } - steps := make([][]byte, 0, len(parts)) - for _, part := range parts { - steps = append(steps, []byte(part)) - } - out.Queries = append(out.Queries, protocol.AuthenticationRequest{Profile: protocol.AuthenticationPathProfile, Root: result.Key.String(), Steps: steps, Operation: "resolve"}) - } - } - out.Candidates = store.ordered - if len(out.Queries) == 0 { - return out, fmt.Errorf("queries are required") - } - return out, nil -} -func run() error { - file := flag.String("input", "", "JSON array of complete file snapshots; file values are base64") - backend := flag.String("backend", "kzg", "kzg or ipa") - chunk := flag.Int("chunk-size", 256<<10, "file chunk bytes") - flag.Parse() - f, err := os.Open(*file) - if err != nil { - return err - } - defer f.Close() - data, err := io.ReadAll(io.LimitReader(f, (256<<20)+1)) - if err != nil { - return err - } - if len(data) > 256<<20 { - return fmt.Errorf("input exceeds256MiB") - } - out, err := compile(context.Background(), data, *backend, *chunk) - if err != nil { - return err - } - return json.NewEncoder(os.Stdout).Encode(out) -} -func main() { - if err := run(); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} diff --git a/tools/evaluation/cmd/malt-eval-rooted-file-trace/main_test.go b/tools/evaluation/cmd/malt-eval-rooted-file-trace/main_test.go deleted file mode 100644 index 2305cbe..0000000 --- a/tools/evaluation/cmd/malt-eval-rooted-file-trace/main_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "testing" - - "github.com/dewebprotocol/malt-core/maltcid" -) - -func TestFileSnapshotsUseRuntimeSchemaAndRetainHistory(t *testing.T) { - data := []byte(`[{"files":{"dir/file":"aGVsbG8gd29ybGQ="},"queries":["dir/file","missing"]},{"files":{},"queries":["dir/file"]}]`) - out, err := compile(t.Context(), data, "ipa", 4) - if err != nil { - t.Fatal(err) - } - if len(out.Queries) != 3 || out.Queries[0].Root == out.Queries[2].Root { - t.Fatal("history lost") - } - positional := false - for _, c := range out.Candidates { - if c.State.Descriptor.Layout == maltcid.Positional { - positional = true - for _, e := range c.State.Entries { - if string(e.Label) == "@payload" { - t.Fatal("positional system") - } - } - } else { - for _, e := range c.State.Entries { - if string(e.Label) == "dir/file" { - t.Fatal("flattened alias") - } - } - } - } - if !positional { - t.Fatal("large file skipped measured sequence") - } - if _, err = compile(t.Context(), []byte(`[{"files":{"a":"","a/b":""},"queries":["a"]}]`), "ipa", 4); err == nil { - t.Fatal("accepted collision") - } -} diff --git a/tools/evaluation/cmd/malt-eval-rq1-fixture-build/main.go b/tools/evaluation/cmd/malt-eval-rq1-fixture-build/main.go deleted file mode 100644 index c355f54..0000000 --- a/tools/evaluation/cmd/malt-eval-rq1-fixture-build/main.go +++ /dev/null @@ -1,540 +0,0 @@ -// Command malt-eval-rq1-fixture-build materializes the paper's shared RQ1 -// payload/query fixture into one token-guarded empty Gateway. It creates one -// UnixFS root for the trusted Path/CAR/Direct-CAS routes and one MALT-KZG root, -// then verifies all D={1,2,4,8} paths before publishing a fixture descriptor. -package main - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "net/http" - "net/url" - "os" - "path/filepath" - "sort" - "strings" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-client/merkledag" - merkledagimport "github.com/dewebprotocol/malt-client/merkledag/importer" - "github.com/dewebprotocol/malt-client/transport" - "github.com/dewebprotocol/malt-core/auth/commitment/kzg" - "github.com/dewebprotocol/malt-core/derivation" - "github.com/dewebprotocol/malt-core/engine" - "github.com/dewebprotocol/malt-core/maltcid" - "github.com/dewebprotocol/malt-core/protocol" - "github.com/dewebprotocol/malt-core/sdk/authentication" - authenticationbuiltin "github.com/dewebprotocol/malt-core/sdk/authentication/builtin" - cid "github.com/ipfs/go-cid" -) - -const ( - fixtureSchema = "malt-rq1-shared-fixture/v1" - descriptorSchema = "malt-rq1-shared-fixture-descriptor/v1" - payloadBytes = 4096 - unixFSChunkBytes = 262144 - maximumResponse = 8 << 20 - requestTimeout = 5 * time.Minute - fixtureIdentifier = "paper-rq1-shared-payload-depths-v1" - unixFSProfile = "unixfs-v1/basic-directory/balanced-file/raw-leaf/fixed-262144" - maltProfile = "canonical-nested-map/radix-256/kzg" - verificationMode = "trusted-path-plus-local-car-plus-local-direct-cas-plus-local-malt-prooflist-payload-binding" - bootstrapObjects = uint32(12) -) - -var exactDepths = []depthFixture{ - {Depth: 1, Segments: []string{"d1"}}, - {Depth: 2, Segments: []string{"d2", "payload"}}, - {Depth: 4, Segments: []string{"d4", "a", "b", "payload"}}, - {Depth: 8, Segments: []string{"d8", "a", "b", "c", "d", "e", "f", "payload"}}, -} - -type depthFixture struct { - Depth int `json:"depth"` - Segments []string `json:"segments"` -} - -type payloadFixture struct { - CID string `json:"cid"` - Bytes uint64 `json:"bytes"` - SHA256 string `json:"sha256"` -} - -type routeRoot struct { - Route string `json:"route"` - Root string `json:"root"` -} - -type fixture struct { - SchemaVersion string `json:"schema_version"` - FixtureID string `json:"fixture_id"` - Payload payloadFixture `json:"payload"` - Depths []depthFixture `json:"depths"` - Routes []routeRoot `json:"routes"` - UnixFSProfile string `json:"unixfs_profile"` - MALTProfile string `json:"malt_profile"` - Verification string `json:"verification"` - BootstrapCount uint32 `json:"bootstrap_object_count"` -} - -type descriptor struct { - SchemaVersion string `json:"schema_version"` - Path string `json:"path"` - SHA256 string `json:"sha256"` - Bytes int64 `json:"bytes"` -} - -type trieNode struct { - edges map[string]*trieEdge -} - -type trieEdge struct { - child *trieNode - payload bool -} - -func main() { - if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil { - fmt.Fprintln(os.Stderr, "malt-eval-rq1-fixture-build:", err) - os.Exit(2) - } -} - -func run(arguments []string, stdout, stderr io.Writer) error { - flags := flag.NewFlagSet("malt-eval-rq1-fixture-build", flag.ContinueOnError) - flags.SetOutput(stderr) - baseURL := flags.String("base-url", "", "token-guarded empty Gateway origin") - instanceToken := flags.String("gateway-instance-token", "", "64-character evaluation instance token") - bootstrapAuthorizationToken := flags.String("gateway-bootstrap-authorization-token", "", "distinct 64-character secret bootstrap capability") - outputPath := flags.String("out", "", "new absolute fixture JSON path") - if err := flags.Parse(arguments); err != nil { - return err - } - if flags.NArg() != 0 || *baseURL == "" || *instanceToken == "" || *bootstrapAuthorizationToken == "" || *outputPath == "" { - return errors.New("usage: malt-eval-rq1-fixture-build --base-url URL --gateway-instance-token TOKEN --gateway-bootstrap-authorization-token SECRET --out NEW.json") - } - if !canonicalToken(*instanceToken) || !canonicalToken(*bootstrapAuthorizationToken) || *instanceToken == *bootstrapAuthorizationToken { - return errors.New("gateway instance and bootstrap authorization tokens must be distinct 64-character lowercase hexadecimal values") - } - parsed, err := url.Parse(strings.TrimRight(*baseURL, "/")) - if err != nil || parsed.Scheme != "http" && parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || parsed.Opaque != "" || - parsed.Path != "" || parsed.RawPath != "" || parsed.RawQuery != "" || parsed.Fragment != "" { - return errors.New("base URL must be an absolute HTTP(S) origin") - } - output, err := filepath.Abs(*outputPath) - if err != nil { - return err - } - evaluation, err := gatewaytransport.New(gatewaytransport.Options{ - BaseURL: parsed.String(), InstanceToken: *instanceToken, HTTPClient: &http.Client{Timeout: requestTimeout}, - }) - if err != nil { - return err - } - httpClient := evaluation.InstanceHTTPClient() - remote, err := transport.New(transport.Options{BaseURL: parsed.String(), HTTPClient: httpClient}) - if err != nil { - return err - } - ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) - defer cancel() - if err := verifyGatewayHealth(ctx, evaluation, *instanceToken); err != nil { - return err - } - payload := deterministicPayload() - payloadCID, err := remote.Put(ctx, payload) - if err != nil { - return fmt.Errorf("store shared payload: %w", err) - } - unixRoot, err := buildUnixFS(ctx, remote, payload) - if err != nil { - return err - } - maltRoot, objects, err := buildMALT(ctx, payloadCID) - if err != nil { - return err - } - if err := bootstrapMALT(ctx, evaluation, *bootstrapAuthorizationToken, objects); err != nil { - return err - } - if err := verifyFixture(ctx, httpClient, parsed.String(), evaluation, remote, unixRoot, maltRoot, payloadCID, payload); err != nil { - return err - } - digest := sha256.Sum256(payload) - value := fixture{ - SchemaVersion: fixtureSchema, FixtureID: fixtureIdentifier, - Payload: payloadFixture{CID: payloadCID.String(), Bytes: uint64(len(payload)), SHA256: hex.EncodeToString(digest[:])}, - Depths: cloneDepths(exactDepths), - Routes: []routeRoot{ - {Route: "trusted-path-gateway", Root: unixRoot.String()}, {Route: "trustless-car", Root: unixRoot.String()}, - {Route: "direct-cas", Root: unixRoot.String()}, {Route: "malt-kzg", Root: maltRoot.String()}, - }, - UnixFSProfile: unixFSProfile, - MALTProfile: maltProfile, - Verification: verificationMode, - BootstrapCount: uint32(len(objects)), - } - if err := value.validate(); err != nil { - return err - } - raw, err := json.Marshal(value) - if err != nil { - return err - } - raw = append(raw, '\n') - if err := publishExclusive(output, raw); err != nil { - return err - } - artifactDigest := sha256.Sum256(raw) - encoder := json.NewEncoder(stdout) - encoder.SetEscapeHTML(false) - return encoder.Encode(descriptor{SchemaVersion: descriptorSchema, Path: output, SHA256: hex.EncodeToString(artifactDigest[:]), Bytes: int64(len(raw))}) -} - -func verifyGatewayHealth(ctx context.Context, evaluation *gatewaytransport.Client, token string) error { - health, err := evaluation.Health(ctx) - if err != nil { - return fmt.Errorf("read Gateway health: %w", err) - } - if health.Status != "ok" || health.EvaluationInstanceToken != token || health.KVBackend != "badger" || health.BlobBackend != "embedded" || - health.ArcTableMode != "versioned" || health.CommitmentProfile != "kzg" || health.EvaluationAuthenticationBootstrap != gatewaytransport.BootstrapProfile { - return errors.New("Gateway does not expose the exact empty RQ1 fixture-build capability") - } - return nil -} - -func deterministicPayload() []byte { - payload := make([]byte, payloadBytes) - for index := range payload { - payload[index] = byte((index*131 + 17) % 251) - } - return payload -} - -func buildUnixFS(ctx context.Context, remote *transport.Client, payload []byte) (cid.Cid, error) { - editor, err := merkledagimport.NewEditor(remote, merkledagimport.Options{ - Model: merkledagimport.ModelUnixFS, FileLayout: merkledagimport.FileLayoutBalanced, - DirLayout: merkledagimport.DirLayoutBasic, ChunkSize: unixFSChunkBytes, RawFileLeaf: true, - }) - if err != nil { - return cid.Undef, err - } - for _, depth := range exactDepths { - if err := editor.PutFile(ctx, strings.Join(depth.Segments, "/"), payload, 0o644); err != nil { - return cid.Undef, fmt.Errorf("materialize UnixFS depth %d: %w", depth.Depth, err) - } - } - root, err := cid.Parse(editor.Root()) - if err != nil { - return cid.Undef, fmt.Errorf("parse UnixFS fixture root: %w", err) - } - return root, nil -} - -func buildMALT(ctx context.Context, payload cid.Cid) (cid.Cid, []protocol.AuthenticationCandidate, error) { - root := &trieNode{edges: map[string]*trieEdge{}} - for _, depth := range exactDepths { - if err := root.insert(depth.Segments); err != nil { - return cid.Undef, nil, err - } - } - scheme, err := kzg.NewScheme() - if err != nil { - return cid.Undef, nil, err - } - profiles := engine.NewRegistry() - if err := profiles.Register(scheme); err != nil { - return cid.Undef, nil, err - } - e := engine.New(profiles) - rootCID, objects, err := commitTrie(ctx, e, root, payload) - if err != nil { - return cid.Undef, nil, err - } - return rootCID, objects, nil -} - -func (node *trieNode) insert(segments []string) error { - if node == nil || len(segments) == 0 { - return errors.New("MALT fixture path is empty") - } - current := node - for index, segment := range segments { - if segment == "" || strings.Contains(segment, "/") { - return fmt.Errorf("invalid MALT fixture segment %q", segment) - } - edge := current.edges[segment] - last := index == len(segments)-1 - if edge == nil { - edge = &trieEdge{} - current.edges[segment] = edge - } - if last { - if edge.child != nil || edge.payload { - return errors.New("MALT fixture path conflicts with an existing path") - } - edge.payload = true - continue - } - if edge.payload { - return errors.New("MALT fixture path crosses a payload leaf") - } - if edge.child == nil { - edge.child = &trieNode{edges: map[string]*trieEdge{}} - } - current = edge.child - } - return nil -} - -func commitTrie(ctx context.Context, e *engine.Engine, node *trieNode, payload cid.Cid) (cid.Cid, []protocol.AuthenticationCandidate, error) { - keys := make([]string, 0, len(node.edges)) - for key := range node.edges { - keys = append(keys, key) - } - sort.Strings(keys) - entries := make(map[string]cid.Cid, len(keys)) - objects := make([]protocol.AuthenticationCandidate, 0) - for _, key := range keys { - edge := node.edges[key] - if edge.payload { - entries[key] = payload - continue - } - childRoot, childObjects, err := commitTrie(ctx, e, edge.child, payload) - if err != nil { - return cid.Undef, nil, err - } - objects = append(objects, childObjects...) - entries[key] = childRoot - } - state := engine.State{Descriptor: maltcid.RootDescriptor{Layout: maltcid.Prefix, DerivationProfile: uint8(derivation.SHA256), Profile: maltcid.KZG4096}, Entries: make([]engine.Entry, 0, len(keys))} - for _, key := range keys { - state.Entries = append(state.Entries, engine.Entry{Label: []byte(key), Target: entries[key]}) - } - candidate, err := authentication.Prepare(ctx, e, state) - if err != nil { - return cid.Undef, nil, err - } - objects = append(objects, candidate) - return cid.MustParse(candidate.Root), objects, nil -} - -func bootstrapMALT(ctx context.Context, evaluation *gatewaytransport.Client, bootstrapAuthorizationToken string, objects []protocol.AuthenticationCandidate) error { - for index, object := range objects { - response, err := evaluation.BootstrapEvaluationObject(ctx, bootstrapAuthorizationToken, gatewaytransport.BootstrapObject{ - OperationID: fmt.Sprintf("rq1-fixture-prefix-%03d", index+1), Candidate: object, - }) - if err != nil { - return fmt.Errorf("bootstrap MALT object %d: %w", index, err) - } - if response.Root.String() != object.Root { - return fmt.Errorf("bootstrap MALT object %d returned a different root/profile", index) - } - } - return nil -} - -func verifyFixture(ctx context.Context, client *http.Client, baseURL string, evaluation *gatewaytransport.Client, remote *transport.Client, unixRoot, maltRoot, payloadCID cid.Cid, payload []byte) error { - verifier, err := authenticationbuiltin.NewVerifier(maltcid.KZG4096) - if err != nil { - return err - } - for _, depth := range exactDepths { - carRequest := merkledag.MerkleDAGReadRequest{ - Profile: merkledag.MerkleDAGReadProfile, Root: unixRoot.String(), Segments: append([]string(nil), depth.Segments...), - } - encodedCARRequest, err := json.Marshal(carRequest) - if err != nil { - return err - } - encodedCAR, err := evaluation.ReadCAR(ctx, encodedCARRequest) - if err != nil { - return fmt.Errorf("read CAR depth %d: %w", depth.Depth, err) - } - carResult, err := merkledag.VerifyMerkleDAGCARRead(ctx, carRequest, encodedCAR) - if err != nil { - return fmt.Errorf("verify CAR depth %d: %w", depth.Depth, err) - } - directResult, err := merkledag.ReadMerkleDAGDirectCASVerified( - ctx, gatewaytransport.RawCASGetter{Client: evaluation}, unixRoot, depth.Segments, nil, nil, - ) - if err != nil { - return fmt.Errorf("verify Direct CAS depth %d: %w", depth.Depth, err) - } - if !carResult.Target.Equals(payloadCID) || !directResult.Target.Equals(payloadCID) || !bytes.Equal(carResult.Data, payload) || !bytes.Equal(directResult.Data, payload) { - return fmt.Errorf("Merkle-DAG routes do not bind the shared payload at depth %d", depth.Depth) - } - if err := verifyTrustedPath(ctx, client, baseURL, unixRoot, depth.Segments, payloadCID, payload); err != nil { - return fmt.Errorf("verify trusted Path depth %d: %w", depth.Depth, err) - } - steps := make([][]byte, len(depth.Segments)) - for i, segment := range depth.Segments { - steps[i] = []byte(segment) - } - request := protocol.AuthenticationRequest{Profile: protocol.AuthenticationPathProfile, Root: maltRoot.String(), Steps: steps, Operation: "resolve"} - result, err := remote.Authenticate(ctx, request) - if err != nil { - return fmt.Errorf("resolve MALT depth %d: %w", depth.Depth, err) - } - valid, err := authentication.Verify(verifier, request, *result) - if err != nil || !valid || result.AbsentStep != nil { - return fmt.Errorf("locally verify MALT depth %d: valid=%t absent=%t: %v", depth.Depth, valid, result.AbsentStep != nil, err) - } - target, err := cid.Parse(result.Resolved) - if err != nil || !target.Equals(payloadCID) { - return fmt.Errorf("MALT depth %d returned a different target", depth.Depth) - } - bound, err := evaluation.GetRawCAS(ctx, target) - if err != nil { - return fmt.Errorf("MALT depth %d payload fetch failed: %w", depth.Depth, err) - } - boundCID, err := target.Prefix().Sum(bound) - if err != nil || !boundCID.Equals(target) { - return fmt.Errorf("MALT depth %d payload bytes do not match target CID %s", depth.Depth, target) - } - if !bytes.Equal(bound, payload) { - return fmt.Errorf("MALT depth %d payload binding returned different bytes", depth.Depth) - } - } - return nil -} - -func verifyTrustedPath(ctx context.Context, client *http.Client, baseURL string, root cid.Cid, segments []string, target cid.Cid, payload []byte) error { - request := struct { - Profile string `json:"profile"` - Root string `json:"root"` - Segments []string `json:"segments"` - }{Profile: merkledag.MerkleDAGReadProfile, Root: root.String(), Segments: append([]string(nil), segments...)} - encoded, err := json.Marshal(request) - if err != nil { - return err - } - httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(baseURL, "/")+"/v1/compat/merkledag/path/read", bytes.NewReader(encoded)) - if err != nil { - return err - } - httpRequest.Header.Set("Content-Type", "application/json") - response, err := client.Do(httpRequest) - if err != nil { - return err - } - defer response.Body.Close() - body, err := io.ReadAll(io.LimitReader(response.Body, maximumResponse+1)) - if err != nil { - return err - } - if response.StatusCode != http.StatusOK || len(body) > maximumResponse || response.Header.Get("X-Malt-MerkleDAG-Target") != target.String() || !bytes.Equal(body, payload) { - return fmt.Errorf("trusted Path response status=%d does not match target/payload", response.StatusCode) - } - return nil -} - -func (value fixture) validate() error { - if value.SchemaVersion != fixtureSchema || value.FixtureID != fixtureIdentifier || value.Payload.Bytes != payloadBytes || !canonicalToken(value.Payload.SHA256) || - len(value.Depths) != len(exactDepths) || len(value.Routes) != 4 || value.BootstrapCount != bootstrapObjects || - value.UnixFSProfile != unixFSProfile || value.MALTProfile != maltProfile || value.Verification != verificationMode { - return errors.New("constructed RQ1 fixture is incomplete") - } - payload := deterministicPayload() - payloadDigest := sha256.Sum256(payload) - if value.Payload.SHA256 != hex.EncodeToString(payloadDigest[:]) { - return errors.New("constructed RQ1 fixture does not bind the deterministic payload digest") - } - payloadCID, err := cid.Parse(value.Payload.CID) - if err != nil || payloadCID.Prefix().Codec != cid.Raw { - return errors.New("constructed RQ1 payload requires a canonical raw CID") - } - expectedPayloadCID, err := payloadCID.Prefix().Sum(payload) - if err != nil || !payloadCID.Equals(expectedPayloadCID) { - return errors.New("constructed RQ1 payload CID does not bind the deterministic bytes") - } - for index, depth := range value.Depths { - if depth.Depth != exactDepths[index].Depth || !equalStrings(depth.Segments, exactDepths[index].Segments) { - return errors.New("constructed RQ1 depth matrix differs from the paper fixture") - } - } - exactRoutes := []string{"trusted-path-gateway", "trustless-car", "direct-cas", "malt-kzg"} - roots := make([]cid.Cid, len(value.Routes)) - for index, route := range value.Routes { - if route.Route != exactRoutes[index] { - return errors.New("constructed RQ1 route order differs from the paper matrix") - } - root, err := cid.Parse(route.Root) - if err != nil { - return fmt.Errorf("parse %s fixture root: %w", route.Route, err) - } - roots[index] = root - } - if roots[0].Prefix().Codec != cid.DagProtobuf || !roots[0].Equals(roots[1]) || !roots[0].Equals(roots[2]) || - !fixturePrefixRoot(roots[3]) { - return errors.New("constructed RQ1 route roots do not preserve shared Merkle/MALT boundaries") - } - return nil -} - -func publishExclusive(path string, raw []byte) (resultErr error) { - if !filepath.IsAbs(path) { - return errors.New("fixture output path must be absolute") - } - if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { - return err - } - file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) - if err != nil { - return err - } - defer func() { - if closeErr := file.Close(); resultErr == nil && closeErr != nil { - resultErr = closeErr - } - if resultErr != nil { - _ = os.Remove(path) - } - }() - if _, err := file.Write(raw); err != nil { - return err - } - return file.Sync() -} - -func cloneDepths(values []depthFixture) []depthFixture { - result := make([]depthFixture, len(values)) - for index, value := range values { - result[index] = depthFixture{Depth: value.Depth, Segments: append([]string(nil), value.Segments...)} - } - return result -} - -func equalStrings(left, right []string) bool { - if len(left) != len(right) { - return false - } - for index := range left { - if left[index] != right[index] { - return false - } - } - return true -} - -func canonicalToken(value string) bool { - if len(value) != sha256.Size*2 { - return false - } - decoded, err := hex.DecodeString(value) - return err == nil && hex.EncodeToString(decoded) == value -} - -func fixturePrefixRoot(root cid.Cid) bool { - descriptor, _, err := maltcid.ParseRoot(root) - return err == nil && descriptor.Layout == maltcid.Prefix && descriptor.DerivationProfile == uint8(derivation.SHA256) && descriptor.Profile == maltcid.KZG4096 -} diff --git a/tools/evaluation/cmd/malt-eval-rq1-fixture-build/main_test.go b/tools/evaluation/cmd/malt-eval-rq1-fixture-build/main_test.go deleted file mode 100644 index a253ec7..0000000 --- a/tools/evaluation/cmd/malt-eval-rq1-fixture-build/main_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package main - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "testing" - - cid "github.com/ipfs/go-cid" - mh "github.com/multiformats/go-multihash" -) - -func TestBuildMALTProducesExactDepthTrie(t *testing.T) { - payload := deterministicPayload() - payloadCID, err := cid.Prefix{Version: 1, Codec: cid.Raw, MhType: mh.SHA2_256, MhLength: -1}.Sum(payload) - if err != nil { - t.Fatal(err) - } - root, objects, err := buildMALT(context.Background(), payloadCID) - if err != nil { - t.Fatal(err) - } - if !fixturePrefixRoot(root) { - t.Fatalf("root = %s", root) - } - // 1 root + (D-1) intermediate Prefix Roots for each disjoint first segment. - if len(objects) != 1+(1-1)+(2-1)+(4-1)+(8-1) { - t.Fatalf("object count = %d", len(objects)) - } - top := objects[len(objects)-1] - if top.Root != root.String() || len(top.State.Entries) != len(exactDepths) { - t.Fatalf("top object = %#v", top) - } - for _, name := range []string{"d1", "d2", "d4", "d8"} { - found := false - for _, entry := range top.State.Entries { - if string(entry.Label) == name && entry.Target.Defined() { - found = true - } - } - if !found { - t.Fatalf("top object lacks %q", name) - } - } -} - -func TestFixtureValidationRequiresSharedCompatibilityRoot(t *testing.T) { - payload := deterministicPayload() - digest := sha256.Sum256(payload) - payloadCID, err := cid.Prefix{Version: 1, Codec: cid.Raw, MhType: mh.SHA2_256, MhLength: -1}.Sum(payload) - if err != nil { - t.Fatal(err) - } - compatibilityRoot, err := cid.Prefix{Version: 1, Codec: cid.DagProtobuf, MhType: mh.SHA2_256, MhLength: -1}.Sum([]byte("compatibility-root")) - if err != nil { - t.Fatal(err) - } - maltRoot, _, err := buildMALT(context.Background(), payloadCID) - if err != nil { - t.Fatal(err) - } - value := fixture{ - SchemaVersion: fixtureSchema, FixtureID: fixtureIdentifier, - Payload: payloadFixture{CID: payloadCID.String(), Bytes: uint64(len(payload)), SHA256: hex.EncodeToString(digest[:])}, - Depths: cloneDepths(exactDepths), - Routes: []routeRoot{ - {Route: "trusted-path-gateway", Root: compatibilityRoot.String()}, {Route: "trustless-car", Root: compatibilityRoot.String()}, - {Route: "direct-cas", Root: compatibilityRoot.String()}, {Route: "malt-kzg", Root: maltRoot.String()}, - }, - UnixFSProfile: unixFSProfile, MALTProfile: maltProfile, Verification: verificationMode, BootstrapCount: bootstrapObjects, - } - if err := value.validate(); err != nil { - t.Fatal(err) - } - value.Routes[1].Root = payloadCID.String() - if err := value.validate(); err == nil { - t.Fatal("different CAR root was accepted") - } -} - -func TestDeterministicPayloadIsStable(t *testing.T) { - first, second := deterministicPayload(), deterministicPayload() - if len(first) != payloadBytes || !bytes.Equal(first, second) { - t.Fatal("fixture payload is not stable") - } - digest := sha256.Sum256(first) - if hex.EncodeToString(digest[:]) != "432f51b0d64d9bcdf2e738ee846a9f5740646e0bb1cc095616293e6cac8bfe40" { - t.Fatalf("payload digest = %x", digest) - } -} diff --git a/tools/evaluation/cmd/malt-eval-rq1-worker/main.go b/tools/evaluation/cmd/malt-eval-rq1-worker/main.go deleted file mode 100644 index e8d1b02..0000000 --- a/tools/evaluation/cmd/malt-eval-rq1-worker/main.go +++ /dev/null @@ -1,667 +0,0 @@ -// Command malt-eval-rq1-worker is the stable process boundary used by the -// evaluator for long-lived warm CAR/Direct-CAS clients. Cold runs start a new -// process. Stdout is JSONL only; diagnostics and flag errors go to stderr. -package main - -import ( - "bufio" - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "net" - "net/http" - "os" - "regexp" - "strings" - "sync" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-client/merkledag" - "github.com/dewebprotocol/malt-client/transport" - cid "github.com/ipfs/go-cid" -) - -const ( - workerRecordSchema = "malt-rq1-worker-record/v3" - workerRequestSchema = "malt-rq1-worker-request/v1" - maxWorkerLineBytes = 1 << 20 - maxWorkerRequests = 1_000_000 - routeCAR = "trustless-car" - routeDirectCAS = "direct-cas" - - evaluationInstanceTokenHeader = gatewaytransport.InstanceTokenHeader - evaluationOperationHeader = "X-Malt-Evaluation-RQ1-Operation" - evaluationRouteHeader = "X-Malt-Evaluation-RQ1-Route" - evaluationLeaseHeader = "X-Malt-Evaluation-RQ1-Lease" - evaluationCacheProfile = "gateway.rq1-cache-observation/v1" -) - -var operationIDPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$`) - -type workerRequest struct { - SchemaVersion string `json:"schema_version"` - OperationID string `json:"operation_id"` - Segments []string `json:"segments"` -} - -type workerRecord struct { - SchemaVersion string `json:"schema_version"` - WorkerID string `json:"worker_id"` - OperationID string `json:"operation_id,omitempty"` - Route string `json:"route"` - Endpoint string `json:"endpoint"` - VerificationRole string `json:"verification_role"` - Root string `json:"root"` - Segments []string `json:"segments"` - Success bool `json:"success"` - FailureClass string `json:"failure_class,omitempty"` - Error string `json:"error,omitempty"` - LatencyNS uint64 `json:"latency_ns"` - Target string `json:"target,omitempty"` - Kind string `json:"kind,omitempty"` - TotalSize uint64 `json:"total_size,omitempty"` - PayloadBytes uint64 `json:"payload_bytes,omitempty"` - PayloadSHA256 string `json:"payload_sha256,omitempty"` - Metrics *workerMetrics `json:"metrics,omitempty"` - CacheObservation *cacheObservation `json:"cache_observation,omitempty"` -} - -type workerMetrics struct { - NetworkRequests uint64 `json:"network_requests"` - CriticalSequentialRounds uint64 `json:"critical_sequential_rounds"` - BlockLoadCalls uint64 `json:"block_load_calls"` - BlocksVerified uint64 `json:"blocks_verified"` - CASGetRequests uint64 `json:"cas_get_requests"` - CASBytes uint64 `json:"cas_bytes"` - CARBytes uint64 `json:"car_bytes"` - CARBlocks uint64 `json:"car_blocks"` - ProofBytes uint64 `json:"proof_bytes"` - ProofSteps uint64 `json:"proof_steps"` - NetworkDurationNS uint64 `json:"network_duration_ns"` - CIDVerifyDurationNS uint64 `json:"cid_verify_duration_ns"` - CARDecodeDurationNS uint64 `json:"car_decode_duration_ns"` - PathReplayDurationNS uint64 `json:"path_replay_duration_ns"` - ProofDecodeDurationNS uint64 `json:"proof_decode_duration_ns"` - ProofVerifyDurationNS uint64 `json:"proof_verify_duration_ns"` - PayloadReadDurationNS uint64 `json:"payload_read_duration_ns"` - PayloadBindingDurationNS uint64 `json:"payload_binding_duration_ns"` - ServerArcTable serverPhaseTotal `json:"server_arc_table"` - ServerMaterialization serverPhaseTotal `json:"server_materialization"` - ServerOpen serverPhaseTotal `json:"server_open"` - - ServerResponseSerialization serverPhaseTotal `json:"server_response_serialization"` - ClientUserCPUNS uint64 `json:"client_user_cpu_ns"` - ClientSystemCPUNS uint64 `json:"client_system_cpu_ns"` - ProcessPeakRSSBytes uint64 `json:"process_peak_rss_bytes"` -} - -type serverPhaseTotal struct { - Operations uint64 `json:"operations"` - Items uint64 `json:"items"` - Bytes uint64 `json:"bytes"` - DurationNS uint64 `json:"duration_ns"` -} - -type cacheObservation struct { - Profile string `json:"profile"` - Action string `json:"action"` - OperationID string `json:"operation_id"` - Route string `json:"route"` - Lease string `json:"lease"` - InstanceToken string `json:"instance_token"` - CompletedOperationsBefore uint64 `json:"completed_operations_before"` - SuccessfulHTTPRequests uint64 `json:"successful_http_requests"` - ProcessState string `json:"process_state"` - ApplicationResultCache string `json:"application_result_cache"` - OSPageCache string `json:"os_page_cache"` - GatewayHealth gatewayHealth `json:"gateway_health"` -} - -type gatewayHealth struct { - Profile string `json:"profile"` - RawSHA256 string `json:"raw_sha256"` - RawBytes uint64 `json:"raw_bytes"` - InstanceToken string `json:"instance_token"` - CacheObservationProfile string `json:"cache_observation_profile"` - ReadPhasesProfile string `json:"read_phases_profile"` - ApplicationResultCache string `json:"application_result_cache"` - OSPageCache string `json:"os_page_cache"` -} - -type workerConfig struct { - baseURL string - route string - root cid.Cid - workerID string - instanceToken string - requestTimeout time.Duration - cacheClient *http.Client - routeHeaders *evaluationHeaderTransport - evaluation *gatewaytransport.Client -} - -func main() { - if err := run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func run(args []string, stdin io.Reader, stdout, stderr io.Writer) error { - flags := flag.NewFlagSet("malt-eval-rq1-worker", flag.ContinueOnError) - flags.SetOutput(stderr) - baseURL := flags.String("base-url", "", "Gateway or direct-CAS base URL") - route := flags.String("route", "", "trustless-car or direct-cas") - rootText := flags.String("root", "", "caller-selected Merkle-DAG root CID") - workerID := flags.String("worker-id", "", "registered process/run identifier") - instanceToken := flags.String("gateway-instance-token", "", "canonical disposable Gateway evaluation instance token") - timeout := flags.Duration("request-timeout", 5*time.Minute, "per-read deadline") - selfTestCorpus := flags.String("self-test-corpus", "", "formal E0 CAR/Direct-CAS correctness corpus") - if err := flags.Parse(args); err != nil { - return err - } - if flags.NArg() != 0 { - return fmt.Errorf("unexpected positional arguments") - } - if *selfTestCorpus != "" { - if *baseURL != "" || *rootText != "" || *workerID != "" || *instanceToken != "" { - return fmt.Errorf("formal E0 self-test cannot be combined with runtime Gateway/root/worker flags") - } - return runClientRouteSelfTest(strings.TrimSpace(*route), *selfTestCorpus, stdout) - } - root, err := cid.Parse(*rootText) - if err != nil { - return fmt.Errorf("invalid -root: %w", err) - } - config := workerConfig{ - baseURL: strings.TrimSpace(*baseURL), route: strings.TrimSpace(*route), root: root, - workerID: strings.TrimSpace(*workerID), instanceToken: strings.TrimSpace(*instanceToken), requestTimeout: *timeout, - } - if err := config.validate(); err != nil { - return err - } - config.routeHeaders = &evaluationHeaderTransport{base: http.DefaultTransport} - evaluation, err := gatewaytransport.New(gatewaytransport.Options{ - BaseURL: config.baseURL, InstanceToken: config.instanceToken, - HTTPClient: &http.Client{Timeout: config.requestTimeout, Transport: config.routeHeaders}, - }) - if err != nil { - return err - } - config.evaluation = evaluation - config.cacheClient = evaluation.InstanceHTTPClient() - encoder := json.NewEncoder(stdout) - encoder.SetEscapeHTML(false) - scanner := bufio.NewScanner(stdin) - scanner.Buffer(make([]byte, 64<<10), maxWorkerLineBytes) - requests := 0 - var health gatewayHealth - var healthErr error - healthObserved := false - for scanner.Scan() { - requests++ - if requests > maxWorkerRequests { - return fmt.Errorf("worker exceeds %d-request process limit", maxWorkerRequests) - } - raw := append([]byte(nil), scanner.Bytes()...) - request, decodeErr := decodeWorkerRequest(raw) - if decodeErr != nil { - if request.OperationID == "" { - request.OperationID = fmt.Sprintf("invalid-input-%06d", requests) - } - if request.Segments == nil { - request.Segments = []string{} - } - record := config.baseRecord(request) - record.FailureClass = "input_invalid" - record.Error = decodeErr.Error() - if err := encoder.Encode(record); err != nil { - return err - } - continue - } - controlCtx, controlCancel := context.WithTimeout(context.Background(), config.requestTimeout) - if !healthObserved { - health, healthErr = config.observeGatewayHealth(controlCtx) - healthObserved = true - } - var cacheBegin cacheObservation - var cacheErr error - if healthErr == nil { - cacheBegin, cacheErr = config.exchangeCacheObservation(controlCtx, "begin", request, "") - } else { - cacheErr = healthErr - } - controlCancel() - if cacheErr == nil { - config.routeHeaders.Set(request.OperationID, config.route, cacheBegin.Lease) - } - usageBefore, usageErr := readProcessUsage() - operationCtx, operationCancel := context.WithTimeout(context.Background(), config.requestTimeout) - started := time.Now() - var result *merkledag.VerifiedReadResult - if cacheErr == nil && usageErr == nil { - switch config.route { - case routeCAR: - carRequest := merkledag.MerkleDAGReadRequest{ - Profile: merkledag.MerkleDAGReadProfile, Root: config.root.String(), - Segments: append([]string(nil), request.Segments...), - } - encodedRequest, encodeErr := json.Marshal(carRequest) - if encodeErr != nil { - err = encodeErr - break - } - networkStarted := time.Now() - encodedCAR, readErr := config.evaluation.ReadCAR(operationCtx, encodedRequest) - networkDuration := time.Since(networkStarted) - if readErr != nil { - err = readErr - break - } - result, err = merkledag.VerifyMerkleDAGCARRead(operationCtx, carRequest, encodedCAR) - if err == nil { - result.Metrics.NetworkRequests = 1 - result.Metrics.NetworkDurationNS = durationNS(networkDuration) - } - case routeDirectCAS: - result, err = merkledag.ReadMerkleDAGDirectCASVerified( - operationCtx, gatewaytransport.RawCASGetter{Client: config.evaluation}, config.root, request.Segments, nil, nil, - ) - } - } - config.routeHeaders.Clear() - operationCancel() - record := config.baseRecord(request) - record.LatencyNS = durationNS(time.Since(started)) - if cacheErr != nil { - record.FailureClass = "cache_observation" - record.Error = cacheErr.Error() - } else if usageErr != nil { - record.FailureClass = "resource_accounting" - record.Error = usageErr.Error() - } else if err != nil { - record.FailureClass = classifyFailure(err) - record.Error = err.Error() - } else { - metrics := newWorkerMetrics(result.Metrics) - usageAfter, usageErr := readProcessUsage() - if usageErr != nil { - record.FailureClass = "resource_accounting" - record.Error = usageErr.Error() - } else if usageErr := addResourceMetrics(&metrics, usageBefore, usageAfter); usageErr != nil { - record.FailureClass = "resource_accounting" - record.Error = usageErr.Error() - } else { - record.Success = true - record.Target = result.Target.String() - record.Kind = result.Kind - record.TotalSize = result.TotalSize - record.PayloadBytes = uint64(len(result.Data)) - digest := sha256.Sum256(result.Data) - record.PayloadSHA256 = hex.EncodeToString(digest[:]) - record.Metrics = &metrics - endCtx, endCancel := context.WithTimeout(context.Background(), config.requestTimeout) - cacheEnd, cacheErr := config.exchangeCacheObservation(endCtx, "end", request, cacheBegin.Lease) - endCancel() - if cacheErr != nil { - record.Success = false - record.FailureClass = "cache_observation" - record.Error = cacheErr.Error() - record.Target = "" - record.Kind = "" - record.TotalSize = 0 - record.PayloadBytes = 0 - record.PayloadSHA256 = "" - record.Metrics = nil - } else { - cacheEnd.GatewayHealth = health - record.CacheObservation = &cacheEnd - } - } - } - if err := encoder.Encode(record); err != nil { - return err - } - } - if err := scanner.Err(); err != nil { - if strings.Contains(err.Error(), "token too long") { - return fmt.Errorf("worker request exceeds %d bytes", maxWorkerLineBytes) - } - return err - } - return nil -} - -func newWorkerMetrics(value merkledag.VerifiedReadMetrics) workerMetrics { - return workerMetrics{ - NetworkRequests: value.NetworkRequests, CriticalSequentialRounds: value.CriticalSequentialRounds, - BlockLoadCalls: value.BlockLoadCalls, BlocksVerified: value.BlocksVerified, - CASGetRequests: value.CASGetRequests, CASBytes: value.CASBytes, CARBytes: value.CARBytes, CARBlocks: value.CARBlocks, - NetworkDurationNS: value.NetworkDurationNS, CIDVerifyDurationNS: value.CIDVerifyDurationNS, - PayloadBindingDurationNS: value.PayloadBindingDurationNS, - CARDecodeDurationNS: value.CARDecodeDurationNS, PathReplayDurationNS: value.PathReplayDurationNS, - PayloadReadDurationNS: value.PayloadReadDurationNS, - } -} - -func (config workerConfig) validate() error { - if config.baseURL == "" || config.workerID == "" { - return fmt.Errorf("-base-url and -worker-id are required") - } - if config.route != routeCAR && config.route != routeDirectCAS { - return fmt.Errorf("-route must be %q or %q", routeCAR, routeDirectCAS) - } - if config.requestTimeout <= 0 { - return fmt.Errorf("-request-timeout must be positive") - } - if !canonicalToken(config.instanceToken) { - return fmt.Errorf("-gateway-instance-token must be 64 lowercase hexadecimal characters") - } - return nil -} - -func (config workerConfig) baseRecord(request workerRequest) workerRecord { - verificationRole := "local-car-cid-link-replay" - if config.route == routeDirectCAS { - verificationRole = "local-direct-cas-cid-link-replay" - } - return workerRecord{ - SchemaVersion: workerRecordSchema, WorkerID: config.workerID, OperationID: request.OperationID, - Route: config.route, Endpoint: "verified-bytes", VerificationRole: verificationRole, - Root: config.root.String(), Segments: append([]string(nil), request.Segments...), - } -} - -func decodeWorkerRequest(data []byte) (workerRequest, error) { - if len(bytes.TrimSpace(data)) == 0 { - return workerRequest{}, fmt.Errorf("worker request is empty") - } - var request workerRequest - if err := decodeStrictJSON(data, &request); err != nil { - return workerRequest{}, fmt.Errorf("decode worker request: %w", err) - } - if request.SchemaVersion != workerRequestSchema { - return request, fmt.Errorf("schema_version must be %q", workerRequestSchema) - } - if !operationIDPattern.MatchString(request.OperationID) { - return request, fmt.Errorf("operation_id does not match the RQ1 worker contract") - } - if request.Segments == nil || len(request.Segments) > 256 { - return request, fmt.Errorf("segments must be an explicit array with at most 256 values") - } - for index, segment := range request.Segments { - if segment == "" || len(segment) > 1024 || strings.Contains(segment, "/") { - return request, fmt.Errorf("invalid segment %d", index) - } - } - return request, nil -} - -func classifyFailure(err error) string { - if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { - return "deadline" - } - var transportErr *transport.Error - if errors.As(err, &transportErr) { - return "remote_rejection" - } - var networkErr net.Error - if errors.As(err, &networkErr) { - return "network" - } - return "local_verification" -} - -func durationNS(value time.Duration) uint64 { - if value <= 0 { - return 0 - } - return uint64(value) -} - -type cacheObservationRequest struct { - Profile string `json:"profile"` - Action string `json:"action"` - OperationID string `json:"operation_id"` - Route string `json:"route"` - Lease string `json:"lease,omitempty"` -} - -func (config workerConfig) exchangeCacheObservation(ctx context.Context, action string, operation workerRequest, lease string) (cacheObservation, error) { - body, err := json.Marshal(cacheObservationRequest{ - Profile: evaluationCacheProfile, Action: action, OperationID: operation.OperationID, Route: config.route, Lease: lease, - }) - if err != nil { - return cacheObservation{}, err - } - request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(config.baseURL, "/")+"/v1/evaluation/rq1/cache-observation", bytes.NewReader(body)) - if err != nil { - return cacheObservation{}, err - } - request.Header.Set("Content-Type", "application/json") - response, err := config.cacheClient.Do(request) - if err != nil { - return cacheObservation{}, err - } - defer response.Body.Close() - encoded, err := io.ReadAll(io.LimitReader(response.Body, (64<<10)+1)) - if err != nil { - return cacheObservation{}, err - } - if len(encoded) > 64<<10 { - return cacheObservation{}, fmt.Errorf("RQ1 cache observation exceeds 65536 bytes") - } - if response.StatusCode < 200 || response.StatusCode >= 300 { - return cacheObservation{}, fmt.Errorf("RQ1 cache observation HTTP %d: %s", response.StatusCode, strings.TrimSpace(string(encoded))) - } - var observation cacheObservation - if err := decodeStrictJSON(encoded, &observation); err != nil { - return cacheObservation{}, fmt.Errorf("decode RQ1 cache observation: %w", err) - } - if observation.Profile != evaluationCacheProfile || observation.Action != action || observation.OperationID != operation.OperationID || - observation.Route != config.route || !canonicalToken(observation.Lease) || observation.InstanceToken != config.instanceToken || - observation.ApplicationResultCache != "none" || observation.OSPageCache != "uncontrolled" || - observation.ProcessState != "fresh-process-first-operation" && observation.ProcessState != "same-process-retained" { - return cacheObservation{}, fmt.Errorf("RQ1 cache observation does not bind the exact operation/instance/cache boundary") - } - if action == "begin" { - if lease != "" || observation.SuccessfulHTTPRequests != 0 { - return cacheObservation{}, fmt.Errorf("RQ1 cache begin has invalid lease/request state") - } - } else if action == "end" { - if observation.Lease != lease || observation.SuccessfulHTTPRequests == 0 { - return cacheObservation{}, fmt.Errorf("RQ1 cache end did not observe a successful concrete route request") - } - } else { - return cacheObservation{}, fmt.Errorf("unsupported RQ1 cache action %q", action) - } - return observation, nil -} - -func (config workerConfig) observeGatewayHealth(ctx context.Context) (gatewayHealth, error) { - request, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(config.baseURL, "/")+"/healthz", nil) - if err != nil { - return gatewayHealth{}, err - } - response, err := config.cacheClient.Do(request) - if err != nil { - return gatewayHealth{}, err - } - defer response.Body.Close() - raw, err := io.ReadAll(io.LimitReader(response.Body, (1<<20)+1)) - if err != nil { - return gatewayHealth{}, err - } - if len(raw) > 1<<20 { - return gatewayHealth{}, fmt.Errorf("Gateway health exceeds 1048576 bytes") - } - if response.StatusCode < 200 || response.StatusCode >= 300 { - return gatewayHealth{}, fmt.Errorf("Gateway health HTTP %d", response.StatusCode) - } - var values map[string]json.RawMessage - if err := decodeStrictJSON(raw, &values); err != nil { - return gatewayHealth{}, fmt.Errorf("decode Gateway health: %w", err) - } - require := func(key, want string) error { - encoded, ok := values[key] - if !ok { - return fmt.Errorf("Gateway health omitted %q", key) - } - var got string - if err := json.Unmarshal(encoded, &got); err != nil || got != want { - return fmt.Errorf("Gateway health %q does not equal %q", key, want) - } - return nil - } - for key, want := range map[string]string{ - "status": "ok", - "evaluation_instance_token": config.instanceToken, - "evaluation_rq1_cache_observation": evaluationCacheProfile, - "evaluation_rq1_read_phases": "gateway.rq1-read-phases/v3", - "evaluation_rq1_application_result_cache": "none", - "evaluation_rq1_os_page_cache": "uncontrolled", - } { - if err := require(key, want); err != nil { - return gatewayHealth{}, err - } - } - digest := sha256.Sum256(raw) - return gatewayHealth{ - Profile: "malt-rq1-gateway-health-observation/v1", RawSHA256: hex.EncodeToString(digest[:]), RawBytes: uint64(len(raw)), - InstanceToken: config.instanceToken, CacheObservationProfile: evaluationCacheProfile, - ReadPhasesProfile: "gateway.rq1-read-phases/v3", ApplicationResultCache: "none", OSPageCache: "uncontrolled", - }, nil -} - -type evaluationHeaderTransport struct { - base http.RoundTripper - - mu sync.RWMutex - operationID string - route string - lease string -} - -func (transport *evaluationHeaderTransport) Set(operationID, route, lease string) { - transport.mu.Lock() - defer transport.mu.Unlock() - transport.operationID = operationID - transport.route = route - transport.lease = lease -} - -func (transport *evaluationHeaderTransport) Clear() { - transport.Set("", "", "") -} - -func (transport *evaluationHeaderTransport) RoundTrip(request *http.Request) (*http.Response, error) { - transport.mu.RLock() - operationID, route, lease := transport.operationID, transport.route, transport.lease - transport.mu.RUnlock() - if operationID == "" || route == "" || lease == "" { - return transport.base.RoundTrip(request) - } - copy := request.Clone(request.Context()) - copy.Header = request.Header.Clone() - copy.Header.Set(evaluationOperationHeader, operationID) - copy.Header.Set(evaluationRouteHeader, route) - copy.Header.Set(evaluationLeaseHeader, lease) - return transport.base.RoundTrip(copy) -} - -func decodeStrictJSON(data []byte, target any) error { - if err := rejectDuplicateJSONKeys(data); err != nil { - return err - } - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.DisallowUnknownFields() - if err := decoder.Decode(target); err != nil { - return err - } - if err := decoder.Decode(&struct{}{}); err != io.EOF { - return fmt.Errorf("expected exactly one JSON object") - } - return nil -} - -func rejectDuplicateJSONKeys(data []byte) error { - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.UseNumber() - if err := scanJSONValue(decoder, "$"); err != nil { - return err - } - if err := decoder.Decode(&struct{}{}); err != io.EOF { - return fmt.Errorf("unexpected trailing JSON") - } - return nil -} - -func scanJSONValue(decoder *json.Decoder, path string) error { - token, err := decoder.Token() - if err != nil { - return err - } - delimiter, ok := token.(json.Delim) - if !ok { - return nil - } - switch delimiter { - case '{': - seen := map[string]struct{}{} - for decoder.More() { - keyToken, err := decoder.Token() - if err != nil { - return err - } - key, ok := keyToken.(string) - if !ok { - return fmt.Errorf("non-string object key at %s", path) - } - if _, duplicate := seen[key]; duplicate { - return fmt.Errorf("duplicate JSON object key %q at %s", key, path) - } - seen[key] = struct{}{} - if err := scanJSONValue(decoder, path+"."+key); err != nil { - return err - } - } - _, err := decoder.Token() - return err - case '[': - for index := 0; decoder.More(); index++ { - if err := scanJSONValue(decoder, fmt.Sprintf("%s[%d]", path, index)); err != nil { - return err - } - } - _, err := decoder.Token() - return err - default: - return fmt.Errorf("unexpected JSON delimiter %q", delimiter) - } -} - -func canonicalToken(value string) bool { - if len(value) != 64 { - return false - } - decoded, err := hex.DecodeString(value) - return err == nil && hex.EncodeToString(decoded) == value -} - -func addResourceMetrics(metrics *workerMetrics, before, after processUsage) error { - if metrics == nil || after.userCPUNS < before.userCPUNS || after.systemCPUNS < before.systemCPUNS || after.peakRSSBytes == 0 { - return fmt.Errorf("invalid process resource counter transition") - } - metrics.ClientUserCPUNS = after.userCPUNS - before.userCPUNS - metrics.ClientSystemCPUNS = after.systemCPUNS - before.systemCPUNS - metrics.ProcessPeakRSSBytes = after.peakRSSBytes - return nil -} diff --git a/tools/evaluation/cmd/malt-eval-rq1-worker/main_test.go b/tools/evaluation/cmd/malt-eval-rq1-worker/main_test.go deleted file mode 100644 index 54a7cdb..0000000 --- a/tools/evaluation/cmd/malt-eval-rq1-worker/main_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package main - -import ( - "bufio" - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" - - cid "github.com/ipfs/go-cid" - mh "github.com/multiformats/go-multihash" -) - -func TestDirectCASWorkerEmitsAtomicVerifiedRecordAndSurvivesBadInput(t *testing.T) { - payload := []byte("locally verified direct CAS payload") - root, err := cid.Prefix{Version: 1, Codec: cid.Raw, MhType: mh.SHA2_256, MhLength: -1}.Sum(payload) - if err != nil { - t.Fatal(err) - } - server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - if request.URL.Path == "/healthz" { - _ = json.NewEncoder(writer).Encode(map[string]string{ - "status": "ok", "evaluation_instance_token": clientTestInstanceToken, - "evaluation_rq1_cache_observation": evaluationCacheProfile, - "evaluation_rq1_read_phases": "gateway.rq1-read-phases/v3", - "evaluation_rq1_application_result_cache": "none", - "evaluation_rq1_os_page_cache": "uncontrolled", - }) - return - } - if request.URL.Path == "/v1/evaluation/rq1/cache-observation" { - serveClientCacheObservation(t, writer, request) - return - } - if request.Method != http.MethodGet || request.URL.Path != "/v1/cas/"+root.String() { - http.NotFound(writer, request) - return - } - if request.Header.Get(evaluationInstanceTokenHeader) != clientTestInstanceToken || request.Header.Get(evaluationOperationHeader) != "read-1" || - request.Header.Get(evaluationRouteHeader) != routeDirectCAS || request.Header.Get(evaluationLeaseHeader) != clientTestLease { - t.Fatalf("route headers = %#v", request.Header) - } - writer.Header().Set("Content-Type", "application/octet-stream") - _, _ = writer.Write(payload) - })) - defer server.Close() - - stdin := strings.NewReader("{\"schema_version\":\"malt-rq1-worker-request/v1\",\"operation_id\":\"read-1\",\"segments\":[]}\n{\"schema_version\":\"malt-rq1-worker-request/v1\",\"operation_id\":\"bad\",\"segments\":[],\"unknown\":true}\n") - var stdout, stderr bytes.Buffer - err = run([]string{ - "-base-url", server.URL, "-route", routeDirectCAS, "-root", root.String(), "-worker-id", "process-1", - "-gateway-instance-token", clientTestInstanceToken, - }, stdin, &stdout, &stderr) - if err != nil { - t.Fatal(err) - } - if stderr.Len() != 0 { - t.Fatalf("stderr = %q", stderr.String()) - } - scanner := bufio.NewScanner(&stdout) - var records []workerRecord - for scanner.Scan() { - var record workerRecord - if err := json.Unmarshal(scanner.Bytes(), &record); err != nil { - t.Fatalf("decode output line: %v", err) - } - records = append(records, record) - } - if err := scanner.Err(); err != nil { - t.Fatal(err) - } - if len(records) != 2 { - t.Fatalf("records = %d, want 2; output=%q", len(records), stdout.String()) - } - verified := records[0] - if !verified.Success || verified.OperationID != "read-1" || verified.Route != routeDirectCAS || verified.Endpoint != "verified-bytes" || verified.VerificationRole != "local-direct-cas-cid-link-replay" || verified.Root != root.String() || - verified.Target != root.String() || verified.PayloadBytes != uint64(len(payload)) || verified.PayloadSHA256 == "" || verified.Metrics == nil { - t.Fatalf("verified record = %#v", verified) - } - if verified.Metrics.CASGetRequests != 1 || verified.Metrics.CriticalSequentialRounds != 1 || verified.Metrics.BlocksVerified != 1 { - t.Fatalf("verified metrics = %#v", verified.Metrics) - } - if verified.Metrics.CIDVerifyDurationNS == 0 || verified.Metrics.PayloadBindingDurationNS == 0 || - verified.Metrics.PayloadBindingDurationNS > verified.Metrics.CIDVerifyDurationNS { - t.Fatalf("verification phase metrics = %#v", verified.Metrics) - } - if verified.Metrics.ProcessPeakRSSBytes == 0 || verified.Metrics.ServerArcTable != (serverPhaseTotal{}) || - verified.Metrics.ServerResponseSerialization != (serverPhaseTotal{}) { - t.Fatalf("resource/server metrics = %#v", verified.Metrics) - } - if verified.CacheObservation == nil || verified.CacheObservation.CompletedOperationsBefore != 0 || - verified.CacheObservation.ProcessState != "fresh-process-first-operation" || verified.CacheObservation.SuccessfulHTTPRequests != 1 || - verified.CacheObservation.GatewayHealth.RawBytes == 0 || verified.CacheObservation.GatewayHealth.OSPageCache != "uncontrolled" { - t.Fatalf("cache observation = %#v", verified.CacheObservation) - } - invalid := records[1] - if invalid.Success || invalid.FailureClass != "input_invalid" || invalid.Error == "" || invalid.Metrics != nil || invalid.CacheObservation != nil { - t.Fatalf("invalid record = %#v", invalid) - } -} - -func TestWorkerRejectsInvalidConfigurationAndOversizedLine(t *testing.T) { - root, err := cid.Prefix{Version: 1, Codec: cid.Raw, MhType: mh.SHA2_256, MhLength: -1}.Sum([]byte("root")) - if err != nil { - t.Fatal(err) - } - if err := run([]string{"-base-url", "http://127.0.0.1", "-route", "trusted", "-root", root.String(), "-worker-id", "p", "-gateway-instance-token", clientTestInstanceToken}, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{}); err == nil { - t.Fatal("unsupported worker route was accepted") - } - line := strings.Repeat("x", maxWorkerLineBytes+1) + "\n" - if err := run([]string{"-base-url", "http://127.0.0.1", "-route", routeDirectCAS, "-root", root.String(), "-worker-id", "p", "-gateway-instance-token", clientTestInstanceToken}, strings.NewReader(line), &bytes.Buffer{}, &bytes.Buffer{}); err == nil { - t.Fatal("oversized worker request was accepted") - } -} - -func serveClientCacheObservation(t *testing.T, writer http.ResponseWriter, request *http.Request) { - t.Helper() - if request.Method != http.MethodPost || request.Header.Get(evaluationInstanceTokenHeader) != clientTestInstanceToken { - http.Error(writer, "invalid instance token", http.StatusForbidden) - return - } - var wire cacheObservationRequest - if err := json.NewDecoder(request.Body).Decode(&wire); err != nil { - http.Error(writer, err.Error(), http.StatusBadRequest) - return - } - requests := uint64(0) - if wire.Action == "end" { - if wire.Lease != clientTestLease { - http.Error(writer, "invalid lease", http.StatusConflict) - return - } - requests = 1 - } - _ = json.NewEncoder(writer).Encode(cacheObservation{ - Profile: evaluationCacheProfile, Action: wire.Action, OperationID: wire.OperationID, Route: wire.Route, - Lease: clientTestLease, InstanceToken: clientTestInstanceToken, CompletedOperationsBefore: 0, - SuccessfulHTTPRequests: requests, ProcessState: "fresh-process-first-operation", - ApplicationResultCache: "none", OSPageCache: "uncontrolled", - }) -} - -const clientTestInstanceToken = "8888888888888888888888888888888888888888888888888888888888888888" -const clientTestLease = "6666666666666666666666666666666666666666666666666666666666666666" - -func TestDecodeWorkerRequestRejectsTrailingValueAndMissingSegments(t *testing.T) { - if _, err := decodeWorkerRequest([]byte(`{"schema_version":"malt-rq1-worker-request/v1","operation_id":"x","segments":[]} {}`)); err == nil { - t.Fatal("trailing JSON was accepted") - } - if _, err := decodeWorkerRequest([]byte(`{"schema_version":"malt-rq1-worker-request/v1","operation_id":"x"}`)); err == nil { - t.Fatal("omitted segments were accepted") - } - if _, err := decodeWorkerRequest([]byte(`{"schema_version":"malt-rq1-worker-request/v1","operation_id":"bad/id","segments":[]}`)); err == nil { - t.Fatal("path-like operation ID was accepted") - } - if _, err := decodeWorkerRequest([]byte(`{"schema_version":"malt-rq1-worker-request/v1","operation_id":"x","segments":["a/b"]}`)); err == nil { - t.Fatal("slash-encoded segment was accepted") - } - if _, err := decodeWorkerRequest([]byte(`{"schema_version":"malt-rq1-worker-request/v1","operation_id":"one","operation_id":"two","segments":[]}`)); err == nil { - t.Fatal("duplicate worker request key was accepted") - } -} - -func TestStrictEvaluationJSONRejectsDuplicateKeys(t *testing.T) { - var value cacheObservation - if err := decodeStrictJSON([]byte(`{"profile":"one","profile":"two"}`), &value); err == nil { - t.Fatal("duplicate cache-observation key was accepted") - } -} diff --git a/tools/evaluation/cmd/malt-eval-rq1-worker/rss_bytes.go b/tools/evaluation/cmd/malt-eval-rq1-worker/rss_bytes.go deleted file mode 100644 index 84463f5..0000000 --- a/tools/evaluation/cmd/malt-eval-rq1-worker/rss_bytes.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build darwin - -package main - -const unixRSSIsKiB = false diff --git a/tools/evaluation/cmd/malt-eval-rq1-worker/rss_kib.go b/tools/evaluation/cmd/malt-eval-rq1-worker/rss_kib.go deleted file mode 100644 index b781503..0000000 --- a/tools/evaluation/cmd/malt-eval-rq1-worker/rss_kib.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build aix || dragonfly || freebsd || linux || netbsd || openbsd || solaris - -package main - -const unixRSSIsKiB = true diff --git a/tools/evaluation/cmd/malt-eval-rq1-worker/selftest.go b/tools/evaluation/cmd/malt-eval-rq1-worker/selftest.go deleted file mode 100644 index 78e7bd7..0000000 --- a/tools/evaluation/cmd/malt-eval-rq1-worker/selftest.go +++ /dev/null @@ -1,452 +0,0 @@ -package main - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/http/httptest" - "os" - "os/exec" - "path/filepath" - "slices" - "strings" - "sync/atomic" - "time" - - casmemory "github.com/dewebprotocol/malt-client/internal/cas/memory" - "github.com/dewebprotocol/malt-client/internal/evaluation/e0selftest" - "github.com/dewebprotocol/malt-client/merkledag" - merkledagimport "github.com/dewebprotocol/malt-client/merkledag/importer" - cid "github.com/ipfs/go-cid" - car "github.com/ipld/go-car/v2" - carstorage "github.com/ipld/go-car/v2/storage" -) - -const ( - clientRouteSelfTestCorpusSchema = "malt-rq1-client-route-correctness-corpus/v1" - clientRouteSelfTestToken = "9999999999999999999999999999999999999999999999999999999999999999" - clientRouteSelfTestLease = "7777777777777777777777777777777777777777777777777777777777777777" -) - -var clientRouteSelfTestProfiles = map[string]e0selftest.Profile{ - routeCAR: { - ProfileID: "rq1-trustless-car-positive-hostile-v1", - PositiveCases: []string{"car-positive"}, - HostileCases: []string{"malformed-request", "car-cid-mismatch", "wrong-path"}, - }, - routeDirectCAS: { - ProfileID: "rq1-direct-cas-positive-hostile-v1", - PositiveCases: []string{"direct-cas-positive"}, - HostileCases: []string{"malformed-request", "cid-content-mismatch", "wrong-path"}, - }, -} - -var clientRouteSelfTestCapabilities = map[string]string{ - routeCAR: "rq1.trustless-car", - routeDirectCAS: "rq1.direct-cas", -} - -type clientRouteSelfTestCorpus struct { - SchemaVersion string `json:"schema_version"` - TrustlessCARPayloadB64 string `json:"trustless_car_payload_base64"` - DirectCASPayloadB64 string `json:"direct_cas_payload_base64"` - Segments []string `json:"segments"` - ChunkBytes int `json:"chunk_bytes"` -} - -type clientRouteFixture struct { - store *casmemory.Store - root cid.Cid - segments []string - payload []byte - evidence []merkledag.MerkleDAGBlock -} - -type recordingBlockGetter struct { - store *casmemory.Store - evidence []merkledag.MerkleDAGBlock -} - -func (g *recordingBlockGetter) Get(ctx context.Context, key cid.Cid) ([]byte, error) { - data, err := g.store.Get(ctx, key) - if err != nil { - return nil, err - } - g.evidence = append(g.evidence, merkledag.MerkleDAGBlock{ - CID: key.String(), Codec: key.Type(), Data: append([]byte(nil), data...), - }) - return data, nil -} - -type selfTestWorkerRunner func(context.Context, string, string, cid.Cid, []string, bool) (workerRecord, error) - -func runClientRouteSelfTest(route, corpusPath string, output io.Writer) error { - return runClientRouteSelfTestWithRunner(route, corpusPath, output, runSelfTestWorkerProcess) -} - -func runClientRouteSelfTestWithRunner(route, corpusPath string, output io.Writer, runner selfTestWorkerRunner) error { - profile, ok := clientRouteSelfTestProfiles[route] - if !ok || runner == nil { - return fmt.Errorf("formal E0 self-test route must be %q or %q", routeCAR, routeDirectCAS) - } - corpus, err := loadClientRouteSelfTestCorpus(corpusPath) - if err != nil { - return err - } - carPayload, directPayload, err := corpus.validate() - if err != nil { - return err - } - payload := carPayload - if route == routeDirectCAS { - payload = directPayload - } - fixture, err := newClientRouteFixture(payload, corpus.Segments, corpus.ChunkBytes) - if err != nil { - return fmt.Errorf("build RQ1 client-route self-test fixture: %w", err) - } - results := make([]e0selftest.CaseResult, 0, len(profile.PositiveCases)+len(profile.HostileCases)) - for _, caseID := range append(append([]string(nil), profile.PositiveCases...), profile.HostileCases...) { - passed, err := runClientRouteSelfTestCase(route, caseID, fixture, runner) - if err != nil { - return fmt.Errorf("RQ1 %s self-test %s: %w", route, caseID, err) - } - if !passed { - return fmt.Errorf("RQ1 %s self-test %s did not observe its required accept/reject behavior", route, caseID) - } - results = append(results, e0selftest.CaseResult{ID: caseID, Passed: true}) - } - receipt, err := e0selftest.Issue(clientRouteSelfTestCapabilities[route], profile, results) - if err != nil { - return err - } - encoder := json.NewEncoder(output) - encoder.SetEscapeHTML(false) - return encoder.Encode(receipt) -} - -func loadClientRouteSelfTestCorpus(path string) (clientRouteSelfTestCorpus, error) { - if !filepath.IsAbs(path) || filepath.Clean(path) != path { - return clientRouteSelfTestCorpus{}, errors.New("RQ1 client-route self-test corpus path must be canonical and absolute") - } - info, err := os.Lstat(path) - if err != nil { - return clientRouteSelfTestCorpus{}, fmt.Errorf("inspect RQ1 client-route self-test corpus: %w", err) - } - if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() || info.Size() <= 0 || info.Size() > maxWorkerLineBytes { - return clientRouteSelfTestCorpus{}, errors.New("RQ1 client-route self-test corpus must be a bounded regular non-symlink file") - } - resolved, err := filepath.EvalSymlinks(path) - if err != nil || resolved != path { - return clientRouteSelfTestCorpus{}, errors.New("RQ1 client-route self-test corpus path or parent is symlinked") - } - data, err := os.ReadFile(path) - if err != nil { - return clientRouteSelfTestCorpus{}, err - } - var corpus clientRouteSelfTestCorpus - if err := decodeStrictJSON(data, &corpus); err != nil { - return clientRouteSelfTestCorpus{}, fmt.Errorf("decode RQ1 client-route self-test corpus: %w", err) - } - return corpus, nil -} - -func (c clientRouteSelfTestCorpus) validate() ([]byte, []byte, error) { - if c.SchemaVersion != clientRouteSelfTestCorpusSchema || - !slices.Equal(c.Segments, []string{"docs", "payload.bin"}) || c.ChunkBytes != 5 { - return nil, nil, errors.New("RQ1 client-route self-test corpus has an invalid schema, exact path, or chunk profile") - } - carPayload, err := base64.StdEncoding.Strict().DecodeString(c.TrustlessCARPayloadB64) - if err != nil || len(carPayload) < c.ChunkBytes*2 { - return nil, nil, errors.New("RQ1 client-route self-test corpus has invalid Trustless-CAR payload bytes") - } - directPayload, err := base64.StdEncoding.Strict().DecodeString(c.DirectCASPayloadB64) - if err != nil || len(directPayload) < c.ChunkBytes*2 { - return nil, nil, errors.New("RQ1 client-route self-test corpus has invalid Direct-CAS payload bytes") - } - if bytes.Equal(carPayload, directPayload) { - return nil, nil, errors.New("RQ1 client-route self-test payloads must be distinct") - } - return carPayload, directPayload, nil -} - -func newClientRouteFixture(payload []byte, segments []string, chunkBytes int) (clientRouteFixture, error) { - store := casmemory.New() - result, err := merkledagimport.ImportFiles(context.Background(), store, []merkledagimport.File{{ - Path: strings.Join(segments, "/"), Data: append([]byte(nil), payload...), Mode: 0o644, - }}, merkledagimport.Options{ - Model: merkledagimport.ModelUnixFS, FileLayout: merkledagimport.FileLayoutBalanced, - DirLayout: merkledagimport.DirLayoutBasic, ChunkSize: chunkBytes, - }) - if err != nil { - return clientRouteFixture{}, err - } - root, err := cid.Parse(result.Root) - if err != nil { - return clientRouteFixture{}, err - } - recorder := &recordingBlockGetter{store: store} - verified, err := merkledag.ReadMerkleDAGDirectCASVerified(context.Background(), recorder, root, segments, nil, nil) - if err != nil { - return clientRouteFixture{}, err - } - if !bytes.Equal(verified.Data, payload) || !verified.Root.Equals(root) || len(recorder.evidence) == 0 { - return clientRouteFixture{}, errors.New("generated RQ1 client-route fixture failed local replay") - } - return clientRouteFixture{ - store: store, root: root, segments: append([]string(nil), segments...), payload: append([]byte(nil), payload...), - evidence: append([]merkledag.MerkleDAGBlock(nil), recorder.evidence...), - }, nil -} - -func runClientRouteSelfTestCase(route, caseID string, fixture clientRouteFixture, runner selfTestWorkerRunner) (bool, error) { - carBytes, err := encodeSelfTestCAR(fixture.root, fixture.evidence, caseID == "car-cid-mismatch") - if err != nil { - return false, err - } - var routeRequests atomic.Uint64 - server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - if serveClientRouteSelfTestControl(writer, request, route, &routeRequests) { - return - } - if !validClientRouteSelfTestHeaders(request, route) { - http.Error(writer, "invalid RQ1 route binding", http.StatusForbidden) - return - } - switch route { - case routeCAR: - if request.Method != http.MethodPost || request.URL.Path != "/v1/compat/merkledag/car/read" || request.Header.Get("Content-Type") != "application/json" { - http.NotFound(writer, request) - return - } - body, readErr := io.ReadAll(io.LimitReader(request.Body, maxWorkerLineBytes+1)) - var wire merkledag.MerkleDAGReadRequest - if readErr != nil || len(body) > maxWorkerLineBytes || decodeStrictJSON(body, &wire) != nil || wire.Root != fixture.root.String() { - http.Error(writer, "invalid CAR read request", http.StatusBadRequest) - return - } - routeRequests.Add(1) - writer.Header().Set("Content-Type", merkledag.MerkleDAGCARReadMediaType) - _, _ = writer.Write(carBytes) - case routeDirectCAS: - if request.Method != http.MethodGet || !strings.HasPrefix(request.URL.Path, "/v1/cas/") { - http.NotFound(writer, request) - return - } - key, parseErr := cid.Parse(strings.TrimPrefix(request.URL.Path, "/v1/cas/")) - if parseErr != nil { - http.Error(writer, "invalid CID", http.StatusBadRequest) - return - } - data, getErr := fixture.store.Get(request.Context(), key) - if getErr != nil { - http.NotFound(writer, request) - return - } - if caseID == "cid-content-mismatch" && key.Equals(fixture.root) { - data[0] ^= 0xff - } - routeRequests.Add(1) - writer.Header().Set("Content-Type", "application/octet-stream") - _, _ = writer.Write(data) - default: - http.NotFound(writer, request) - } - })) - defer server.Close() - - segments := append([]string(nil), fixture.segments...) - if caseID == "wrong-path" { - segments = []string{"docs", "missing.bin"} - } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - record, err := runner(ctx, server.URL, route, fixture.root, segments, caseID == "malformed-request") - if err != nil { - return false, err - } - requests := routeRequests.Load() - if record.Route != route || record.Root != fixture.root.String() || record.Endpoint != "verified-bytes" { - return false, nil - } - switch caseID { - case "car-positive", "direct-cas-positive": - digest := sha256.Sum256(fixture.payload) - role := "local-car-cid-link-replay" - if route == routeDirectCAS { - role = "local-direct-cas-cid-link-replay" - } - if !record.Success || record.VerificationRole != role || record.PayloadBytes != uint64(len(fixture.payload)) || - record.PayloadSHA256 != hex.EncodeToString(digest[:]) || record.Metrics == nil || requests == 0 { - return false, nil - } - if record.Metrics.CIDVerifyDurationNS == 0 || record.Metrics.PayloadBindingDurationNS == 0 || - record.Metrics.PayloadBindingDurationNS > record.Metrics.CIDVerifyDurationNS { - return false, nil - } - if route == routeCAR { - return record.Metrics.NetworkRequests == 1 && record.Metrics.CARBlocks > 0 && record.Metrics.BlocksVerified > 0, nil - } - return record.Metrics.CASGetRequests > 0 && record.Metrics.CASGetRequests == record.Metrics.BlocksVerified, nil - case "malformed-request": - return !record.Success && record.FailureClass == "input_invalid" && requests == 0, nil - case "car-cid-mismatch", "cid-content-mismatch", "wrong-path": - return !record.Success && record.FailureClass == "local_verification" && record.Error != "" && requests > 0, nil - default: - return false, fmt.Errorf("unknown RQ1 client-route self-test case %q", caseID) - } -} - -func encodeSelfTestCAR(root cid.Cid, evidence []merkledag.MerkleDAGBlock, corrupt bool) ([]byte, error) { - var output bytes.Buffer - writable, err := carstorage.NewWritable(&output, []cid.Cid{root}, car.WriteAsCarV1(true)) - if err != nil { - return nil, err - } - for index, item := range evidence { - key, err := cid.Parse(item.CID) - if err != nil { - return nil, err - } - data := append([]byte(nil), item.Data...) - if corrupt && index == 0 { - data[0] ^= 0xff - } - if err := writable.Put(context.Background(), key.KeyString(), data); err != nil { - return nil, err - } - } - if err := writable.Finalize(); err != nil { - return nil, err - } - return output.Bytes(), nil -} - -func serveClientRouteSelfTestControl(writer http.ResponseWriter, request *http.Request, route string, routeRequests *atomic.Uint64) bool { - switch request.URL.Path { - case "/healthz": - _ = json.NewEncoder(writer).Encode(map[string]string{ - "status": "ok", "evaluation_instance_token": clientRouteSelfTestToken, - "evaluation_rq1_cache_observation": evaluationCacheProfile, - "evaluation_rq1_read_phases": "gateway.rq1-read-phases/v3", - "evaluation_rq1_application_result_cache": "none", - "evaluation_rq1_os_page_cache": "uncontrolled", - }) - return true - case "/v1/evaluation/rq1/cache-observation": - if request.Method != http.MethodPost || request.Header.Get(evaluationInstanceTokenHeader) != clientRouteSelfTestToken { - http.Error(writer, "invalid RQ1 cache observation request", http.StatusForbidden) - return true - } - body, err := io.ReadAll(io.LimitReader(request.Body, (64<<10)+1)) - var wire cacheObservationRequest - if err != nil || len(body) > 64<<10 || decodeStrictJSON(body, &wire) != nil || wire.Profile != evaluationCacheProfile || wire.Route != route || wire.OperationID != "read-1" { - http.Error(writer, "invalid RQ1 cache observation binding", http.StatusBadRequest) - return true - } - requests := uint64(0) - if wire.Action == "end" { - if wire.Lease != clientRouteSelfTestLease { - http.Error(writer, "invalid RQ1 cache lease", http.StatusConflict) - return true - } - requests = routeRequests.Load() - } else if wire.Action != "begin" || wire.Lease != "" || routeRequests.Load() != 0 { - http.Error(writer, "invalid RQ1 cache action", http.StatusConflict) - return true - } - _ = json.NewEncoder(writer).Encode(cacheObservation{ - Profile: evaluationCacheProfile, Action: wire.Action, OperationID: wire.OperationID, Route: wire.Route, - Lease: clientRouteSelfTestLease, InstanceToken: clientRouteSelfTestToken, CompletedOperationsBefore: 0, - SuccessfulHTTPRequests: requests, ProcessState: "fresh-process-first-operation", - ApplicationResultCache: "none", OSPageCache: "uncontrolled", - }) - return true - default: - return false - } -} - -func validClientRouteSelfTestHeaders(request *http.Request, route string) bool { - return request.Header.Get(evaluationInstanceTokenHeader) == clientRouteSelfTestToken && - request.Header.Get(evaluationOperationHeader) == "read-1" && request.Header.Get(evaluationRouteHeader) == route && - request.Header.Get(evaluationLeaseHeader) == clientRouteSelfTestLease -} - -func runSelfTestWorkerProcess(ctx context.Context, baseURL, route string, root cid.Cid, segments []string, malformed bool) (workerRecord, error) { - executable, err := os.Executable() - if err != nil { - return workerRecord{}, err - } - input, args, err := selfTestWorkerInvocation(baseURL, route, root, segments, malformed) - if err != nil { - return workerRecord{}, err - } - command := exec.CommandContext(ctx, executable, args...) - command.Stdin = bytes.NewReader(input) - var stdout, stderr bytes.Buffer - command.Stdout, command.Stderr = &stdout, &stderr - if err := command.Run(); err != nil { - return workerRecord{}, fmt.Errorf("run RQ1 production worker process: %w (stderr=%q)", err, strings.TrimSpace(stderr.String())) - } - if stderr.Len() != 0 { - return workerRecord{}, fmt.Errorf("RQ1 production worker emitted stderr %q", strings.TrimSpace(stderr.String())) - } - return decodeSelfTestWorkerRecord(stdout.Bytes()) -} - -func runSelfTestWorkerInProcess(ctx context.Context, baseURL, route string, root cid.Cid, segments []string, malformed bool) (workerRecord, error) { - if err := ctx.Err(); err != nil { - return workerRecord{}, err - } - input, args, err := selfTestWorkerInvocation(baseURL, route, root, segments, malformed) - if err != nil { - return workerRecord{}, err - } - var stdout, stderr bytes.Buffer - if err := run(args, bytes.NewReader(input), &stdout, &stderr); err != nil { - return workerRecord{}, fmt.Errorf("run nested RQ1 worker: %w (stderr=%q)", err, strings.TrimSpace(stderr.String())) - } - if stderr.Len() != 0 { - return workerRecord{}, fmt.Errorf("nested RQ1 worker emitted stderr %q", strings.TrimSpace(stderr.String())) - } - return decodeSelfTestWorkerRecord(stdout.Bytes()) -} - -func selfTestWorkerInvocation(baseURL, route string, root cid.Cid, segments []string, malformed bool) ([]byte, []string, error) { - request := workerRequest{SchemaVersion: workerRequestSchema, OperationID: "read-1", Segments: append([]string(nil), segments...)} - input, err := json.Marshal(request) - if err != nil { - return nil, nil, err - } - if malformed { - input = []byte(`{"schema_version":"malt-rq1-worker-request/v1","operation_id":"read-1","segments":[],"unknown":true}`) - } - input = append(input, '\n') - args := []string{ - "-base-url", baseURL, "-route", route, "-root", root.String(), "-worker-id", "e0-process", - "-gateway-instance-token", clientRouteSelfTestToken, "-request-timeout", "20s", - } - return input, args, nil -} - -func decodeSelfTestWorkerRecord(data []byte) (workerRecord, error) { - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.DisallowUnknownFields() - var record workerRecord - if err := decoder.Decode(&record); err != nil { - return workerRecord{}, err - } - var trailing json.RawMessage - if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { - return workerRecord{}, errors.New("RQ1 worker process emitted trailing output") - } - return record, nil -} diff --git a/tools/evaluation/cmd/malt-eval-rq1-worker/selftest_test.go b/tools/evaluation/cmd/malt-eval-rq1-worker/selftest_test.go deleted file mode 100644 index 3f6101c..0000000 --- a/tools/evaluation/cmd/malt-eval-rq1-worker/selftest_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package main - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/dewebprotocol/malt-client/internal/evaluation/e0selftest" -) - -func TestClientRouteFormalE0SelfTestsIssueBoundReceipts(t *testing.T) { - corpus := writeClientRouteSelfTestCorpus(t) - executable, err := os.Executable() - if err != nil { - t.Fatal(err) - } - for _, route := range []string{routeCAR, routeDirectCAS} { - t.Run(route, func(t *testing.T) { - profile := clientRouteSelfTestProfiles[route] - environment, err := e0selftest.BuildInvocationEnvironment( - clientRouteSelfTestCapabilities[route], profile, - e0selftest.InputFile{FileID: "rq1-client-route-worker", Path: executable}, - []e0selftest.InputFile{{FileID: "rq1-client-route-corpus", Path: corpus}}, - []string{"rq1-client-route-corpus"}, - ) - if err != nil { - t.Fatal(err) - } - t.Setenv(e0selftest.InvocationEnvironment, environment) - var output bytes.Buffer - if err := runClientRouteSelfTestWithRunner(route, corpus, &output, runSelfTestWorkerInProcess); err != nil { - t.Fatal(err) - } - var receipt e0selftest.Receipt - if err := json.Unmarshal(output.Bytes(), &receipt); err != nil { - t.Fatal(err) - } - if err := receipt.Validate(); err != nil { - t.Fatal(err) - } - if receipt.CapabilityID != clientRouteSelfTestCapabilities[route] || receipt.Positive.Executed != 1 || receipt.Hostile.Executed != 3 || - receipt.TestedExecutable.FileID != "rq1-client-route-worker" || len(receipt.TestedInputs) != 1 || receipt.TestedInputs[0].FileID != "rq1-client-route-corpus" { - t.Fatalf("receipt = %+v", receipt) - } - }) - } -} - -func TestClientRouteFormalE0RejectsCapabilityRelabelAndRuntimeFlags(t *testing.T) { - corpus := writeClientRouteSelfTestCorpus(t) - executable, err := os.Executable() - if err != nil { - t.Fatal(err) - } - profile := clientRouteSelfTestProfiles[routeCAR] - environment, err := e0selftest.BuildInvocationEnvironment( - clientRouteSelfTestCapabilities[routeDirectCAS], profile, - e0selftest.InputFile{FileID: "rq1-client-route-worker", Path: executable}, - []e0selftest.InputFile{{FileID: "rq1-client-route-corpus", Path: corpus}}, - []string{"rq1-client-route-corpus"}, - ) - if err != nil { - t.Fatal(err) - } - t.Setenv(e0selftest.InvocationEnvironment, environment) - var output bytes.Buffer - if err := runClientRouteSelfTestWithRunner(routeCAR, corpus, &output, runSelfTestWorkerInProcess); err == nil || !strings.Contains(err.Error(), "does not match E0 invocation") { - t.Fatalf("route/capability relabel error = %v", err) - } - if output.Len() != 0 { - t.Fatalf("relabel emitted receipt %q", output.String()) - } - if err := run([]string{"-route", routeCAR, "-self-test-corpus", corpus, "-base-url", "http://127.0.0.1"}, bytes.NewReader(nil), &bytes.Buffer{}, &bytes.Buffer{}); err == nil { - t.Fatal("formal self-test accepted runtime Gateway flags") - } -} - -func TestClientRouteSelfTestProfileDigestsAreStable(t *testing.T) { - want := map[string]string{ - routeCAR: "89fbb5b13a51fe0b1a2fdc867f0aa27cb5f9be86aaaa0ef9eaf3239769e799f2", - routeDirectCAS: "04cd42d5dfeef2607600e0594ab11fbc1edf05ca563e752006d0cfc42f5d4d1a", - } - for route, profile := range clientRouteSelfTestProfiles { - contract, err := profile.Contract() - if err != nil { - t.Fatal(err) - } - if contract.ProfileSHA256 != want[route] { - t.Fatalf("%s profile digest = %s", route, contract.ProfileSHA256) - } - } -} - -func TestClientRouteSelfTestCorpusRejectsDrift(t *testing.T) { - path := writeClientRouteSelfTestCorpus(t) - corpus, err := loadClientRouteSelfTestCorpus(path) - if err != nil { - t.Fatal(err) - } - if _, _, err := corpus.validate(); err != nil { - t.Fatal(err) - } - corpus.Segments = []string{"payload.bin"} - if _, _, err := corpus.validate(); err == nil { - t.Fatal("client-route corpus accepted a changed semantic path") - } - corpus, err = loadClientRouteSelfTestCorpus(path) - if err != nil { - t.Fatal(err) - } - corpus.DirectCASPayloadB64 = corpus.TrustlessCARPayloadB64 - if _, _, err := corpus.validate(); err == nil { - t.Fatal("client-route corpus accepted route-aliased payloads") - } -} - -func writeClientRouteSelfTestCorpus(t *testing.T) string { - t.Helper() - value := clientRouteSelfTestCorpus{ - SchemaVersion: clientRouteSelfTestCorpusSchema, - TrustlessCARPayloadB64: base64.StdEncoding.EncodeToString([]byte("unit fixture for CAR verification")), - DirectCASPayloadB64: base64.StdEncoding.EncodeToString([]byte("unit fixture for direct CAS replay")), - Segments: []string{"docs", "payload.bin"}, ChunkBytes: 5, - } - data, err := json.Marshal(value) - if err != nil { - t.Fatal(err) - } - path := filepath.Join(t.TempDir(), "rq1-client-route-corpus.json") - if err := os.WriteFile(path, data, 0o600); err != nil { - t.Fatal(err) - } - return path -} diff --git a/tools/evaluation/cmd/malt-eval-rq1-worker/usage_other.go b/tools/evaluation/cmd/malt-eval-rq1-worker/usage_other.go deleted file mode 100644 index decffcb..0000000 --- a/tools/evaluation/cmd/malt-eval-rq1-worker/usage_other.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris - -package main - -import "fmt" - -type processUsage struct { - userCPUNS uint64 - systemCPUNS uint64 - peakRSSBytes uint64 -} - -func readProcessUsage() (processUsage, error) { - return processUsage{}, fmt.Errorf("client CPU and peak RSS accounting is unavailable on this platform") -} diff --git a/tools/evaluation/cmd/malt-eval-rq1-worker/usage_unix.go b/tools/evaluation/cmd/malt-eval-rq1-worker/usage_unix.go deleted file mode 100644 index 3fd9f1a..0000000 --- a/tools/evaluation/cmd/malt-eval-rq1-worker/usage_unix.go +++ /dev/null @@ -1,34 +0,0 @@ -//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris - -package main - -import "golang.org/x/sys/unix" - -type processUsage struct { - userCPUNS uint64 - systemCPUNS uint64 - peakRSSBytes uint64 -} - -func readProcessUsage() (processUsage, error) { - var value unix.Rusage - if err := unix.Getrusage(unix.RUSAGE_SELF, &value); err != nil { - return processUsage{}, err - } - peak := uint64(value.Maxrss) - if unixRSSIsKiB { - peak *= 1024 - } - return processUsage{ - userCPUNS: timevalNS(value.Utime), - systemCPUNS: timevalNS(value.Stime), - peakRSSBytes: peak, - }, nil -} - -func timevalNS(value unix.Timeval) uint64 { - if value.Sec < 0 || value.Usec < 0 { - return 0 - } - return uint64(value.Sec)*1_000_000_000 + uint64(value.Usec)*1_000 -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-browser-wasm/main.go b/tools/evaluation/cmd/malt-eval-rq2-browser-wasm/main.go deleted file mode 100644 index e627128..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-browser-wasm/main.go +++ /dev/null @@ -1,499 +0,0 @@ -//go:build js && wasm - -// Command malt-eval-rq2-browser-wasm runs retained typed authentication Writers -// inside a browser. Its Promise-based JSON RPC keeps candidate verification, -// payload CID binding, exact receipt validation, and retained-root state live -// inside WebAssembly rather than in the native browser driver. -package main - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strings" - "syscall/js" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/authenticationgraph" - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2metrics" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2wire" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2write" - "github.com/dewebprotocol/malt-client/transport" - "github.com/dewebprotocol/malt-core/auth/commitment/ipa" - "github.com/dewebprotocol/malt-core/auth/commitment/kzg" - "github.com/dewebprotocol/malt-core/engine" - cid "github.com/ipfs/go-cid" - mh "github.com/multiformats/go-multihash" -) - -const ( - maxBrowserFixtureBytes = 32 << 20 -) - -var ( - registeredFunctions []js.Func - state browserWriter -) - -type initializeRequest struct { - GatewayBaseURL string `json:"gateway_base_url"` - GatewayInstanceToken string `json:"gateway_instance_token"` - FixtureURL string `json:"fixture_url"` - Backend string `json:"backend"` -} - -type initializeResponse struct { - ParameterLoadNS uint64 `json:"parameter_load_ns"` - ParameterLoadBytes uint64 `json:"parameter_load_bytes"` - ParameterProfile string `json:"parameter_profile"` - ParameterSHA256 string `json:"parameter_sha256"` - FixtureBytes uint64 `json:"fixture_bytes"` - FixtureSHA256 string `json:"fixture_sha256"` -} - -type browserWriter struct { - initialized bool - backend string - remote *transport.Client - app *authenticationgraph.Session - fixture *rq2fixture.Fixture - source map[string][]byte - root cid.Cid - serial uint64 - receiptCount uint64 - sessionID string -} - -func main() { - initialize := js.FuncOf(func(_ js.Value, args []js.Value) any { - return newPromise(args, func(raw string) (string, error) { - var request initializeRequest - if err := strictJSON([]byte(raw), &request); err != nil { - return "", err - } - result, err := state.initialize(request) - if err != nil { - return "", err - } - return marshalJSON(result) - }) - }) - exchange := js.FuncOf(func(_ js.Value, args []js.Value) any { - return newPromise(args, func(raw string) (string, error) { - started := time.Now() - var request rq2wire.WorkerRequest - if err := strictJSON([]byte(raw), &request); err != nil { - return "", err - } - if err := request.Validate(); err != nil { - return "", err - } - record := state.exchange(request) - result, err := marshalJSON(record) - js.Global().Set("maltRQ2LastExecutionNS", durationNS(time.Since(started))) - return result, err - }) - }) - registeredFunctions = append(registeredFunctions, initialize, exchange) - js.Global().Set("maltRQ2Initialize", initialize) - js.Global().Set("maltRQ2Exchange", exchange) - js.Global().Set("maltRQ2LastExecutionNS", uint64(0)) - js.Global().Set("maltRQ2WASMReady", true) - select {} -} - -func newPromise(args []js.Value, run func(string) (string, error)) js.Value { - promise := js.Global().Get("Promise") - if len(args) != 1 || args[0].Type() != js.TypeString { - return promise.Call("reject", "RQ2 WASM RPC requires one JSON string") - } - raw := args[0].String() - var executor js.Func - executor = js.FuncOf(func(_ js.Value, callbacks []js.Value) any { - resolve, reject := callbacks[0], callbacks[1] - go func() { - result, err := run(raw) - if err != nil { - reject.Invoke(err.Error()) - return - } - resolve.Invoke(result) - }() - return nil - }) - value := promise.New(executor) - executor.Release() - return value -} - -func (w *browserWriter) initialize(request initializeRequest) (initializeResponse, error) { - if w.initialized { - return initializeResponse{}, fmt.Errorf("RQ2 WASM writer is already initialized") - } - if request.Backend != "kzg" && request.Backend != "ipa" || request.GatewayBaseURL == "" || request.FixtureURL == "" || !canonicalSHA256(request.GatewayInstanceToken) { - return initializeResponse{}, fmt.Errorf("RQ2 WASM initialization is incomplete") - } - fixture, err := fetchFixture(request.FixtureURL) - if err != nil { - return initializeResponse{}, err - } - sourceFixture, err := rq2fixture.Decode(fixture) - if err != nil { - return initializeResponse{}, err - } - operations := make([]string, 0, len(browserOperations)) - for operation := range browserOperations { - operations = append(operations, operation) - } - if _, err := sourceFixture.Root(request.Backend); err != nil { - return initializeResponse{}, err - } - if err := sourceFixture.RequireOperations(operations); err != nil { - return initializeResponse{}, err - } - started := time.Now() - var scheme engine.Profile - if request.Backend == "kzg" { - scheme, err = kzg.NewScheme() - } else { - scheme, err = ipa.NewScheme() - } - parameterLoadNS := durationNS(time.Since(started)) - if err != nil { - return initializeResponse{}, fmt.Errorf("initialize %s parameters: %w", request.Backend, err) - } - parameterProfile, parameterSHA256, parameterBytes, ok := rq2wire.ParameterEvidence(request.Backend) - if !ok { - return initializeResponse{}, fmt.Errorf("commitment parameter provenance is unavailable") - } - evaluation, err := gatewaytransport.New(gatewaytransport.Options{ - BaseURL: request.GatewayBaseURL, InstanceToken: request.GatewayInstanceToken, - HTTPClient: &http.Client{Timeout: 24 * time.Hour}, - }) - if err != nil { - return initializeResponse{}, err - } - remote, err := transport.New(transport.Options{ - BaseURL: request.GatewayBaseURL, HTTPClient: evaluation.InstanceHTTPClient(), - }) - if err != nil { - return initializeResponse{}, err - } - health, err := evaluation.Health(context.Background()) - if err != nil || health.Status != "ok" || health.EvaluationInstanceToken != request.GatewayInstanceToken { - return initializeResponse{}, fmt.Errorf("browser Gateway health did not echo the exact disposable instance token") - } - registry := engine.NewRegistry() - if err := registry.Register(scheme); err != nil { - return initializeResponse{}, err - } - app, err := authenticationgraph.New(evaluation, engine.New(registry)) - if err != nil { - return initializeResponse{}, err - } - digest := sha256.Sum256(fixture) - *w = browserWriter{ - initialized: true, backend: request.Backend, remote: remote, app: app, - fixture: sourceFixture, - } - return initializeResponse{ - ParameterLoadNS: parameterLoadNS, ParameterLoadBytes: parameterBytes, - ParameterProfile: parameterProfile, ParameterSHA256: parameterSHA256, - FixtureBytes: uint64(len(fixture)), FixtureSHA256: hex.EncodeToString(digest[:]), - }, nil -} - -func fetchFixture(url string) ([]byte, error) { - request, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) - if err != nil { - return nil, err - } - request.Header.Set("Cache-Control", "no-store") - response, err := http.DefaultClient.Do(request) - if err != nil { - return nil, fmt.Errorf("fetch browser fixture: %w", err) - } - defer response.Body.Close() - if response.StatusCode != http.StatusOK || !strings.Contains(response.Header.Get("Cache-Control"), "no-store") { - return nil, fmt.Errorf("browser fixture response is not a non-cacheable success") - } - data, err := io.ReadAll(io.LimitReader(response.Body, maxBrowserFixtureBytes+1)) - if err != nil { - return nil, err - } - if len(data) == 0 || len(data) > maxBrowserFixtureBytes { - return nil, fmt.Errorf("browser fixture size is outside 1..%d", maxBrowserFixtureBytes) - } - return data, nil -} - -func (w *browserWriter) exchange(request rq2wire.WorkerRequest) rq2wire.WorkerRecord { - if !w.initialized || request.ClientKind != rq2wire.ClientBrowserWASM || request.Backend != w.backend { - return rq2wire.FailedRecord(request, "capability_unavailable", fmt.Errorf("WASM writer coordinate was not initialized")) - } - switch request.RecordKind { - case rq2wire.RecordSessionStart: - return w.start(request) - case rq2wire.RecordMutation: - return w.mutate(request) - case rq2wire.RecordSessionEnd: - return w.end(request) - default: - return rq2wire.FailedRecord(request, "input_invalid", fmt.Errorf("preflight is owned by the browser host")) - } -} - -func (w *browserWriter) start(request rq2wire.WorkerRequest) rq2wire.WorkerRecord { - if w.root.Defined() || w.sessionID != "" { - return rq2wire.FailedRecord(request, "session_state", fmt.Errorf("browser session already started")) - } - root, err := cid.Parse(request.ExpectedAcceptedRoot) - if err != nil || !rq2wire.ValidTypedRoot(root.String(), w.backend) { - return rq2wire.FailedRecord(request, "input_invalid", fmt.Errorf("browser root is not a typed %s root", w.backend)) - } - if w.fixture == nil || request.FixtureID != w.fixture.FixtureID { - return rq2wire.FailedRecord(request, "fixture_incompatible", fmt.Errorf("browser request fixture identity does not match the pinned source fixture")) - } - load, err := w.app.Load(context.Background(), root) - if err != nil { - return rq2wire.FailedRecord(request, classifyFailure(err), err) - } - view, err := w.app.Snapshot() - if err != nil || w.fixture.ValidateInitialGraph(view, w.backend) != nil { - if err == nil { - err = w.fixture.ValidateInitialGraph(view, w.backend) - } - return rq2wire.FailedRecord(request, "fixture_incompatible", err) - } - w.root, w.sessionID, w.source = root, request.SessionID, w.fixture.InitialSource() - record := rq2wire.BaseRecord(request) - record.Success = true - record.Session = &rq2wire.SessionEvidence{AcceptedRoot: root.String(), GraphFetch: measured(load.FetchNS, load.WireBytes, load.Objects), GraphImport: measured(load.ImportNS, load.WireBytes, load.Objects)} - return record -} - -var browserOperations = map[string]struct{}{ - "document-edit-cid-binding-submit": {}, "list-append": {}, "list-replace": {}, "map-insert": {}, "map-replace": {}, -} - -func (w *browserWriter) mutate(request rq2wire.WorkerRequest) rq2wire.WorkerRecord { - if _, ok := browserOperations[request.Operation]; !ok { - return rq2wire.FailedRecord(request, "operation_unsupported", fmt.Errorf("unsupported browser operation %q", request.Operation)) - } - if request.SessionID != w.sessionID || !w.root.Defined() || request.ExpectedAcceptedRoot != w.root.String() { - return rq2wire.FailedRecord(request, "root_continuity", fmt.Errorf("browser request does not match retained root/session")) - } - mutationStarted := time.Now() - viewSnapshotStarted := time.Now() - view, err := w.app.Snapshot() - viewSnapshotNS := durationNS(time.Since(viewSnapshotStarted)) - if err != nil { - return rq2wire.FailedRecord(request, "verification_failed", err) - } - if err := w.fixture.ValidateGraphAgainstSource(view, w.backend, w.source); err != nil { - return rq2wire.FailedRecord(request, "fixture_incompatible", fmt.Errorf("browser source pre-image binding: %w", err)) - } - operation, err := w.fixture.Operation(request.Operation) - if err != nil { - return rq2wire.FailedRecord(request, "fixture_incompatible", err) - } - prepared, err := w.prepare(operation) - if err != nil { - return rq2wire.FailedRecord(request, "fixture_incompatible", err) - } - payloadUpload, err := w.remote.PutBatchMeasured(context.Background(), []transport.Block{{Codec: cid.Raw, Data: prepared.payload}}) - results := payloadUpload.Results - if err != nil || len(results) != 1 || !results[0].CID.Equals(prepared.payloadCID) { - if err == nil { - err = fmt.Errorf("browser CAS upload did not return the exact local payload CID") - } - return rq2wire.FailedRecord(request, classifyFailure(err), err) - } - projectionStarted := time.Now() - edit, err := w.app.Begin() - if err != nil { - return rq2wire.FailedRecord(request, "verification_failed", err) - } - candidate, err := rq2write.Apply(context.Background(), edit, w.root, operation, []cid.Cid{prepared.payloadCID}) - projectionNS := durationNS(time.Since(projectionStarted)) - if err != nil { - return rq2wire.FailedRecord(request, "projection_invalid", err) - } - result, err := w.app.Submit(context.Background(), operationID(request), edit, candidate) - if err != nil { - return rq2wire.FailedRecord(request, classifyFailure(err), err) - } - if result.Idempotent { - return rq2wire.FailedRecord(request, "gateway_instance_reused", fmt.Errorf("disposable Gateway returned an idempotent authentication batch replay")) - } - mutationTotalNS := durationNS(time.Since(mutationStarted)) - postView, err := w.app.Snapshot() - if err != nil { - return rq2wire.FailedRecord(request, "verification_failed", fmt.Errorf("snapshot browser post-image: %w", err)) - } - if err := w.fixture.ValidateGraphAgainstSource(postView, w.backend, prepared.postSource); err != nil { - return rq2wire.FailedRecord(request, "verification_failed", fmt.Errorf("browser full source post-image oracle: %w", err)) - } - encodedCandidate := result.Root.String() - parsedCandidate, err := cid.Parse(encodedCandidate) - if err != nil || !parsedCandidate.Equals(result.Root) { - return rq2wire.FailedRecord(request, "verification_failed", fmt.Errorf("expected-root CID encoding did not round-trip")) - } - record := rq2wire.BaseRecord(request) - record.Success = true - metrics, err := browserMetrics(prepared, result, viewSnapshotNS, projectionNS, payloadUpload, mutationTotalNS) - if err != nil { - return rq2wire.FailedRecord(request, "measurement_invalid", err) - } - record.Mutation = &rq2wire.MutationEvidence{ - Operation: request.Operation, PriorRoot: result.Base.String(), CandidateRoot: encodedCandidate, - ReceiptRoot: result.Receipt.Root, ReceiptAccepted: result.Receipt.Root == result.Root.String(), - BatchSHA256: result.Receipt.Digest, - Metrics: metrics, - } - w.root = result.Root - w.source = prepared.postSource - w.serial++ - w.receiptCount++ - return record -} - -func (w *browserWriter) end(request rq2wire.WorkerRequest) rq2wire.WorkerRecord { - if request.SessionID != w.sessionID || !w.root.Defined() || request.ExpectedAcceptedRoot != w.root.String() { - return rq2wire.FailedRecord(request, "root_continuity", fmt.Errorf("browser session-end root does not match retained root")) - } - if err := w.app.Audit(context.Background()); err != nil { - return rq2wire.FailedRecord(request, "audit_failed", err) - } - record := rq2wire.BaseRecord(request) - record.Success = true - record.Session = &rq2wire.SessionEvidence{AcceptedRoot: w.root.String(), ReceiptCount: w.receiptCount, AuditPassed: true, GraphFetch: rq2wire.NotApplicablePhase(), GraphImport: rq2wire.NotApplicablePhase()} - return record -} - -type browserPayload struct { - payload []byte - payloadCID cid.Cid - postSource map[string][]byte - scan rq2wire.PhaseMeasurement - chunk rq2wire.PhaseMeasurement - hash rq2wire.PhaseMeasurement -} - -func (w *browserWriter) prepare(operation rq2fixture.Operation) (browserPayload, error) { - prepared := browserPayload{ - scan: rq2wire.NotApplicablePhase(), chunk: rq2wire.NotApplicablePhase(), hash: rq2wire.NotApplicablePhase(), - } - postSource, payloads, err := w.fixture.ApplySourceOperation(w.source, operation, w.serial) - if err != nil || len(payloads) != 1 { - if err == nil { - err = fmt.Errorf("browser operation must produce exactly one changed payload") - } - return browserPayload{}, err - } - prepared.postSource, prepared.payload = postSource, payloads[0] - if operation.Kind == rq2fixture.KindDocumentEdit { - scanStarted := time.Now() - body := append([]byte(nil), prepared.payload...) - prepared.scan = measured(durationNS(time.Since(scanStarted)), uint64(len(body)), 1) - chunkStarted := time.Now() - chunkSize := 256 << 10 - chunkCount := max(1, (len(body)+chunkSize-1)/chunkSize) - for offset := 0; offset < len(body); offset += chunkSize { - _ = append([]byte(nil), body[offset:min(len(body), offset+chunkSize)]...) - } - prepared.chunk = measured(durationNS(time.Since(chunkStarted)), uint64(len(body)), uint64(chunkCount)) - } - hashStarted := time.Now() - digest, err := mh.Sum(prepared.payload, mh.SHA2_256, -1) - if err != nil { - return browserPayload{}, err - } - prepared.payloadCID = cid.NewCidV1(cid.Raw, digest) - prepared.hash = measured(durationNS(time.Since(hashStarted)), uint64(len(prepared.payload)), 1) - return prepared, nil -} - -func browserMetrics(payload browserPayload, result authenticationgraph.Result, snapshotNS, projectionNS uint64, payloadUpload transport.PutBatchMeasurement, mutationTotalNS uint64) (rq2wire.MutationMetrics, error) { - phase := func(duration, bytes, count uint64) rq2wire.PhaseMeasurement { - return measured(duration, bytes, max(uint64(1), count)) - } - generation, err := rq2metrics.AddDurations(snapshotNS, projectionNS) - if err != nil { - return rq2wire.MutationMetrics{}, err - } - computation, submission := result.Computation, result.Submission - return rq2wire.MutationMetrics{ - TaxonomyProfile: rq2metrics.TaxonomyProfile, MutationTotal: phase(mutationTotalNS, 0, 1), Scan: payload.scan, Chunk: payload.chunk, Hash: payload.hash, - GraphSnapshot: phase(snapshotNS, 0, 1), CandidateApply: phase(computation.ApplyNS, 0, computation.Candidates), CandidateExport: phase(computation.ExportNS, computation.CandidateBytes, computation.Candidates), CandidateGeneration: phase(generation, 0, 1), - BatchEncoding: phase(submission.RequestEncodingNS, submission.RequestWireBytes, 1), Upload: phase(payloadUpload.RoundTripNS, payloadUpload.RequestWireBytes, 1), - GatewayValidateStage: phase(submission.Gateway.ValidationAndStageNS, 0, 1), GatewayPersist: phase(submission.Gateway.PersistNS, 0, 1), ReceiptCheck: phase(submission.ResponseVerifyNS, submission.ResponseWireBytes, 1), - CPUTotal: rq2wire.NotApplicablePhase(), PeakMemory: rq2wire.NotApplicablePhase(), WASMDownload: rq2wire.NotApplicablePhase(), WASMInstantiate: rq2wire.NotApplicablePhase(), ParameterLoad: rq2wire.NotApplicablePhase(), FirstMutation: rq2wire.NotApplicablePhase(), JSWASMBoundary: rq2wire.NotApplicablePhase(), - }, nil -} - -func measured(duration, bytes, count uint64) rq2wire.PhaseMeasurement { - return rq2wire.ObservedPhase(duration, bytes, count) -} - -func canonicalSHA256(value string) bool { - if len(value) != 64 { - return false - } - for _, char := range value { - if char < '0' || char > '9' { - if char < 'a' || char > 'f' { - return false - } - } - } - return true -} - -func operationID(request rq2wire.WorkerRequest) string { - digest := sha256.Sum256([]byte(request.WorkerID + "\x00" + request.SessionID + "\x00" + request.RequestID + "\x00" + request.Operation)) - return "rq2-" + hex.EncodeToString(digest[:16]) -} - -func classifyFailure(err error) string { - var transportError *transport.Error - if errors.As(err, &transportError) { - return "gateway_rejected" - } - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return "timeout" - } - return "authentication_failed" -} - -func durationNS(value time.Duration) uint64 { - if value <= 0 { - return 0 - } - return uint64(value) -} - -func strictJSON(data []byte, target any) error { - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.DisallowUnknownFields() - if err := decoder.Decode(target); err != nil { - return err - } - if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { - return fmt.Errorf("JSON contains trailing data") - } - return nil -} - -func marshalJSON(value any) (string, error) { - data, err := json.Marshal(value) - return string(data), err -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-browser-wasm/main_unsupported.go b/tools/evaluation/cmd/malt-eval-rq2-browser-wasm/main_unsupported.go deleted file mode 100644 index 8473209..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-browser-wasm/main_unsupported.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !js || !wasm - -package main - -import "fmt" - -func main() { - fmt.Println("malt-eval-rq2-browser-wasm must be built with GOOS=js GOARCH=wasm") -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/artifacts.go b/tools/evaluation/cmd/malt-eval-rq2-browser-worker/artifacts.go deleted file mode 100644 index f5e7ced..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/artifacts.go +++ /dev/null @@ -1,134 +0,0 @@ -package main - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - "io" - "os" - "path/filepath" -) - -const ( - maxFixtureBytes = 32 << 20 - maxWASMBytes = 256 << 20 - maxSupportBytes = 8 << 20 -) - -type artifactPin struct { - path string - sha256 string - bytes int64 - executable bool - allowSymlink bool - maxBytes int64 -} - -type artifactSnapshot struct { - pin artifactPin - resolvedPath string - symlink string -} - -func loadPinnedArtifact(pin artifactPin, retain bool) (artifactSnapshot, []byte, error) { - if !filepath.IsAbs(pin.path) || !canonicalSHA256(pin.sha256) || pin.bytes <= 0 || pin.maxBytes <= 0 || pin.bytes > pin.maxBytes { - return artifactSnapshot{}, nil, fmt.Errorf("invalid artifact pin for %q", pin.path) - } - lstat, err := os.Lstat(pin.path) - if err != nil { - return artifactSnapshot{}, nil, err - } - symlink := "" - resolved := pin.path - if lstat.Mode()&os.ModeSymlink != 0 { - if !pin.allowSymlink { - return artifactSnapshot{}, nil, fmt.Errorf("artifact %q must not be a symlink", pin.path) - } - symlink, err = os.Readlink(pin.path) - if err != nil { - return artifactSnapshot{}, nil, err - } - resolved, err = filepath.EvalSymlinks(pin.path) - if err != nil { - return artifactSnapshot{}, nil, err - } - } else if !lstat.Mode().IsRegular() { - return artifactSnapshot{}, nil, fmt.Errorf("artifact %q is not a regular file", pin.path) - } - file, err := os.Open(resolved) - if err != nil { - return artifactSnapshot{}, nil, err - } - defer file.Close() - opened, err := file.Stat() - if err != nil || !opened.Mode().IsRegular() { - return artifactSnapshot{}, nil, fmt.Errorf("artifact %q did not open as a regular file", pin.path) - } - if pin.executable && opened.Mode()&0o111 == 0 { - return artifactSnapshot{}, nil, fmt.Errorf("artifact %q is not executable", pin.path) - } - hash := sha256.New() - var data []byte - var writer io.Writer = hash - if retain { - data = make([]byte, 0, pin.bytes) - writer = io.MultiWriter(hash, byteAppender{target: &data}) - } - written, err := io.Copy(writer, io.LimitReader(file, pin.maxBytes+1)) - if err != nil { - return artifactSnapshot{}, nil, err - } - if written != pin.bytes || written != opened.Size() || hex.EncodeToString(hash.Sum(nil)) != pin.sha256 { - return artifactSnapshot{}, nil, fmt.Errorf("artifact %q does not match its SHA-256/size pin", pin.path) - } - return artifactSnapshot{pin: pin, resolvedPath: resolved, symlink: symlink}, data, nil -} - -func (s artifactSnapshot) verify() error { - current, _, err := loadPinnedArtifact(s.pin, false) - if err != nil { - return err - } - if current.resolvedPath != s.resolvedPath || current.symlink != s.symlink { - return fmt.Errorf("artifact %q changed its resolved identity", s.pin.path) - } - return nil -} - -type byteAppender struct{ target *[]byte } - -func (w byteAppender) Write(value []byte) (int, error) { - *w.target = append(*w.target, value...) - return len(value), nil -} - -func readFixture(path string) ([]byte, error) { - if !filepath.IsAbs(path) { - return nil, fmt.Errorf("fixture path must be absolute") - } - lstat, err := os.Lstat(path) - if err != nil || lstat.Mode()&os.ModeSymlink != 0 || !lstat.Mode().IsRegular() { - return nil, fmt.Errorf("RQ2 browser fixture is not a regular non-symlink file: %v", err) - } - if lstat.Size() <= 0 || lstat.Size() > maxFixtureBytes { - return nil, fmt.Errorf("RQ2 browser fixture size must be in 1..%d bytes", maxFixtureBytes) - } - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() - opened, err := file.Stat() - if err != nil || !opened.Mode().IsRegular() || !os.SameFile(lstat, opened) { - return nil, fmt.Errorf("RQ2 browser fixture changed before it was opened") - } - data, err := io.ReadAll(io.LimitReader(file, maxFixtureBytes+1)) - if err != nil { - return nil, err - } - post, err := os.Lstat(path) - if err != nil || !os.SameFile(lstat, post) || post.Mode()&os.ModeSymlink != 0 || post.Size() != lstat.Size() || int64(len(data)) != lstat.Size() { - return nil, fmt.Errorf("RQ2 browser fixture changed while it was read") - } - return data, nil -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/browser.go b/tools/evaluation/cmd/malt-eval-rq2-browser-worker/browser.go deleted file mode 100644 index abd66d6..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/browser.go +++ /dev/null @@ -1,669 +0,0 @@ -package main - -import ( - "bytes" - "context" - "crypto/rand" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "log" - "net" - "net/http" - "net/http/httputil" - "net/url" - "os" - "os/exec" - "regexp" - "runtime" - "slices" - "strings" - "sync" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/machine" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2wire" -) - -const ( - maxBrowserLogBytes = 64 << 10 - browserForceTerminationBound = 2 * time.Second -) - -var ( - devtoolsPattern = regexp.MustCompile(`DevTools listening on (ws://[^[:space:]]+)`) - versionPattern = regexp.MustCompile(`^(Chromium|Google Chrome(?: for Testing)?) ([0-9]+(?:\.[0-9]+){1,3})(?:[[:space:]].*)?$`) -) - -type browserLaunchConfig struct { - path string - arguments []string - engine string - timeout time.Duration - gatewayURL string - token string - backend string - wasmSHA256 string -} - -type coldStartMetrics struct { - WASMDownloadNS uint64 - WASMDownloadBytes uint64 - WASMInstantiateNS uint64 - ParameterLoadNS uint64 - ParameterBytes uint64 - ParameterProfile string - ParameterSHA256 string -} - -type browserCall struct { - RecordJSON string `json:"record_json"` - CallNS uint64 `json:"call_ns"` - BoundaryNS uint64 `json:"boundary_ns"` - HeapBytes uint64 `json:"heap_bytes"` - WASMMemory uint64 `json:"wasm_memory_bytes"` -} - -type browserStatus struct { - Phase string `json:"phase"` - Error string `json:"error,omitempty"` - WASMSHA256 string `json:"wasm_sha256,omitempty"` - DownloadNS uint64 `json:"download_ns,omitempty"` - DownloadByte uint64 `json:"download_bytes,omitempty"` - Instantiate uint64 `json:"instantiate_ns,omitempty"` - Initialize *struct { - ParameterLoadNS uint64 `json:"parameter_load_ns"` - ParameterLoadBytes uint64 `json:"parameter_load_bytes"` - ParameterProfile string `json:"parameter_profile"` - ParameterSHA256 string `json:"parameter_sha256"` - FixtureBytes uint64 `json:"fixture_bytes"` - FixtureSHA256 string `json:"fixture_sha256"` - } `json:"initialize,omitempty"` -} - -type browserSession struct { - command *exec.Cmd - process *processCompletion - stderr *boundedLog - cdp *cdpClient - server *http.Server - listener net.Listener - profile string - engine string - cold coldStartMetrics - processID int - closeOnce sync.Once - closeErr error -} - -type processCompletion struct { - done chan struct{} - err error -} - -func detectBrowserEngine(ctx context.Context, config browserLaunchConfig) (string, error) { - arguments := append([]string(nil), config.arguments...) - arguments = append(arguments, "--version") - command := exec.CommandContext(ctx, config.path, arguments...) - output, err := command.CombinedOutput() - if err != nil { - return "", fmt.Errorf("run browser version probe: %w: %s", err, boundedText(output, 1024)) - } - engine, err := parseBrowserEngineOutput(string(output)) - if err != nil { - return "", err - } - if config.engine != engine { - return "", fmt.Errorf("browser engine/version is %q, registered %q", engine, config.engine) - } - return engine, nil -} - -func parseBrowserEngineOutput(output string) (string, error) { - line := strings.TrimSpace(output) - matches := versionPattern.FindStringSubmatch(line) - if len(matches) != 3 { - return "", fmt.Errorf("unsupported browser version output %q", boundedText([]byte(output), 1024)) - } - prefix := "chromium-" - if strings.HasPrefix(matches[1], "Google Chrome") { - prefix = "chrome-" - } - return prefix + matches[2], nil -} - -func startBrowser(ctx context.Context, config browserLaunchConfig, fixture, wasm, wasmExec []byte) (*browserSession, error) { - // The version subprocess is part of browser startup and must obey the same - // bound as DevTools discovery and WASM initialization. The Chromium session - // itself remains explicitly owned and closed by browserSession. - startupContext, cancel := context.WithTimeout(ctx, config.timeout) - defer cancel() - engine, err := detectBrowserEngine(startupContext, config) - if err != nil { - return nil, err - } - tokenBytes := make([]byte, 16) - if _, err := rand.Read(tokenBytes); err != nil { - return nil, err - } - prefix := "/rq2-" + hex.EncodeToString(tokenBytes) - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - return nil, err - } - failed := true - defer func() { - if failed { - _ = listener.Close() - } - }() - baseURL := "http://" + listener.Addr().String() + prefix - page, err := browserPage(browserPageConfig{ - GatewayURL: baseURL + "/gateway", FixtureURL: baseURL + "/fixture", WASMURL: baseURL + "/writer.wasm", - WASMSHA256: config.wasmSHA256, GatewayToken: config.token, Backend: config.backend, - }) - if err != nil { - return nil, err - } - target, err := url.Parse(config.gatewayURL) - if err != nil { - return nil, err - } - proxy := httputil.NewSingleHostReverseProxy(target) - proxy.ErrorLog = log.New(io.Discard, "", 0) - proxy.ErrorHandler = func(response http.ResponseWriter, _ *http.Request, cause error) { - response.Header().Set("Cache-Control", "no-store") - http.Error(response, "Gateway proxy failed: "+cause.Error(), http.StatusBadGateway) - } - mux := http.NewServeMux() - serveBytes := func(contentType string, data []byte) http.HandlerFunc { - return func(response http.ResponseWriter, request *http.Request) { - if request.Method != http.MethodGet { - response.Header().Set("Allow", http.MethodGet) - http.Error(response, "method not allowed", http.StatusMethodNotAllowed) - return - } - response.Header().Set("Cache-Control", "no-store, max-age=0") - response.Header().Set("Content-Type", contentType) - response.Header().Set("X-Content-Type-Options", "nosniff") - _, _ = response.Write(data) - } - } - mux.HandleFunc(prefix+"/", serveBytes("text/html; charset=utf-8", page)) - mux.HandleFunc(prefix+"/wasm_exec.js", serveBytes("text/javascript; charset=utf-8", wasmExec)) - mux.HandleFunc(prefix+"/writer.wasm", serveBytes("application/wasm", wasm)) - mux.HandleFunc(prefix+"/fixture", serveBytes("application/octet-stream", fixture)) - mux.Handle(prefix+"/gateway/", http.StripPrefix(prefix+"/gateway", proxy)) - server := &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second, ErrorLog: log.New(io.Discard, "", 0)} - serveErr := make(chan error, 1) - go func() { - serveErr <- server.Serve(listener) - }() - - profile, err := os.MkdirTemp("", "malt-rq2-browser-profile-") - if err != nil { - _ = server.Close() - return nil, err - } - arguments := append([]string(nil), config.arguments...) - arguments = append(arguments, - "--headless=new", "--no-sandbox", "--disable-gpu", "--disable-background-networking", - "--disable-component-update", "--disable-default-apps", "--disable-extensions", "--disable-sync", - "--enable-precise-memory-info", "--metrics-recording-only", "--no-first-run", "--password-store=basic", - "--remote-allow-origins=*", "--remote-debugging-port=0", "--user-data-dir="+profile, - baseURL+"/", - ) - command := exec.CommandContext(ctx, config.path, arguments...) - configureBrowserProcess(command) - command.Stdout = io.Discard - stderr := &boundedLog{limit: maxBrowserLogBytes} - stderrPipe, err := command.StderrPipe() - if err != nil { - _ = server.Close() - _ = os.RemoveAll(profile) - return nil, err - } - if err := command.Start(); err != nil { - _ = server.Close() - _ = os.RemoveAll(profile) - return nil, err - } - process := &processCompletion{done: make(chan struct{})} - go func() { - process.err = command.Wait() - close(process.done) - }() - devtoolsURL := make(chan string, 1) - go scanBrowserLog(stderrPipe, stderr, devtoolsURL) - session := &browserSession{ - command: command, process: process, stderr: stderr, server: server, listener: listener, profile: profile, - engine: engine, processID: command.Process.Pid, - } - var browserWebsocket string - select { - case browserWebsocket = <-devtoolsURL: - case <-process.done: - _ = session.close() - return nil, fmt.Errorf("browser exited before DevTools became ready: %v%s", process.err, formatBrowserLog(stderr)) - case err := <-serveErr: - _ = session.close() - return nil, fmt.Errorf("browser host HTTP server failed: %w", err) - case <-startupContext.Done(): - _ = session.close() - return nil, fmt.Errorf("browser DevTools startup: %w%s", startupContext.Err(), formatBrowserLog(stderr)) - } - pageWebsocket, err := findPageWebsocket(startupContext, browserWebsocket, baseURL+"/") - if err != nil { - _ = session.close() - return nil, fmt.Errorf("find browser page: %w%s", err, formatBrowserLog(stderr)) - } - client, err := newCDPClient(startupContext, pageWebsocket) - if err != nil { - _ = session.close() - return nil, fmt.Errorf("connect browser DevTools: %w", err) - } - session.cdp = client - if err := client.call(startupContext, "Performance.enable", map[string]any{}, nil); err != nil { - _ = session.close() - return nil, err - } - status, err := waitBrowserReady(startupContext, client) - if err != nil { - _ = session.close() - return nil, fmt.Errorf("initialize real browser/WASM writer: %w%s", err, formatBrowserLog(stderr)) - } - fixtureDigest := sha256.Sum256(fixture) - parameterProfile, parameterSHA256, parameterBytes, parameterOK := rq2wire.ParameterEvidence(config.backend) - if status.WASMSHA256 != config.wasmSHA256 || status.DownloadByte != uint64(len(wasm)) || status.DownloadNS == 0 || status.Instantiate == 0 || - status.Initialize == nil || status.Initialize.ParameterLoadNS == 0 || !parameterOK || - status.Initialize.ParameterLoadBytes != parameterBytes || status.Initialize.ParameterProfile != parameterProfile || status.Initialize.ParameterSHA256 != parameterSHA256 || - status.Initialize.FixtureBytes != uint64(len(fixture)) || status.Initialize.FixtureSHA256 != hex.EncodeToString(fixtureDigest[:]) { - _ = session.close() - return nil, fmt.Errorf("browser cold-start provenance/timing evidence is incomplete") - } - session.cold = coldStartMetrics{ - WASMDownloadNS: status.DownloadNS, WASMDownloadBytes: status.DownloadByte, - WASMInstantiateNS: status.Instantiate, ParameterLoadNS: status.Initialize.ParameterLoadNS, - ParameterBytes: status.Initialize.ParameterLoadBytes, - ParameterProfile: status.Initialize.ParameterProfile, ParameterSHA256: status.Initialize.ParameterSHA256, - } - failed = false - return session, nil -} - -func (b *browserSession) exchange(ctx context.Context, request rq2wire.WorkerRequest) (rq2wire.WorkerRecord, browserCall, uint64, uint64, uint64, error) { - rawRequest, err := json.Marshal(request) - if err != nil { - return rq2wire.WorkerRecord{}, browserCall{}, 0, 0, 0, err - } - beforeCPU, err := b.cdp.taskDuration(ctx) - if err != nil { - return rq2wire.WorkerRecord{}, browserCall{}, 0, 0, 0, err - } - sampler, err := startResidentSampler(b.processID) - if err != nil { - return rq2wire.WorkerRecord{}, browserCall{}, 0, 0, 0, err - } - quoted, _ := json.Marshal(string(rawRequest)) - returned, callErr := b.cdp.evaluateString(ctx, "window.maltHostExchange("+string(quoted)+")", true) - peak, samples, sampleErr := sampler.stop() - if callErr != nil { - return rq2wire.WorkerRecord{}, browserCall{}, 0, 0, 0, callErr - } - if sampleErr != nil { - return rq2wire.WorkerRecord{}, browserCall{}, 0, 0, 0, sampleErr - } - afterCPU, err := b.cdp.taskDuration(ctx) - if err != nil || afterCPU < beforeCPU { - return rq2wire.WorkerRecord{}, browserCall{}, 0, 0, 0, fmt.Errorf("invalid Chromium TaskDuration delta") - } - var call browserCall - if err := strictJSON([]byte(returned), &call); err != nil { - return rq2wire.WorkerRecord{}, browserCall{}, 0, 0, 0, err - } - if call.RecordJSON == "" || call.CallNS == 0 { - return rq2wire.WorkerRecord{}, browserCall{}, 0, 0, 0, fmt.Errorf("browser/WASM boundary omitted call evidence") - } - var record rq2wire.WorkerRecord - if err := strictJSON([]byte(call.RecordJSON), &record); err != nil { - return rq2wire.WorkerRecord{}, browserCall{}, 0, 0, 0, err - } - if call.HeapBytes > peak { - peak = call.HeapBytes - } - if call.WASMMemory > peak { - peak = call.WASMMemory - } - cpuNS := secondsToNS(afterCPU - beforeCPU) - return record, call, cpuNS, peak, max(uint64(1), samples), nil -} - -func (b *browserSession) close() error { - b.closeOnce.Do(func() { - if b.cdp != nil { - closeContext, cancel := context.WithTimeout(context.Background(), 2*time.Second) - _ = b.cdp.call(closeContext, "Browser.close", map[string]any{}, nil) - cancel() - _ = b.cdp.Close() - } - if b.server != nil { - shutdownContext, cancel := context.WithTimeout(context.Background(), 2*time.Second) - if err := b.server.Shutdown(shutdownContext); err != nil && !errors.Is(err, http.ErrServerClosed) { - b.closeErr = errors.Join(b.closeErr, err) - } - cancel() - } - if b.command != nil && b.command.Process != nil && b.process != nil { - select { - case <-b.process.done: - if b.process.err != nil && !strings.Contains(b.process.err.Error(), "signal: killed") { - b.closeErr = errors.Join(b.closeErr, b.process.err) - } - case <-time.After(3 * time.Second): - _ = forceKillBrowserProcess(b.command) - <-b.process.done - } - } - if b.profile != "" { - b.closeErr = errors.Join(b.closeErr, os.RemoveAll(b.profile)) - } - }) - return b.closeErr -} - -// forceTerminate is the failure-only process boundary. It sends an immediate -// non-graceful kill before attempting CDP or HTTP cleanup, then requires both -// process reaping and connection teardown to complete within one fixed bound. -// Normal successful sessions continue to use close's graceful path. -func (b *browserSession) forceTerminate() error { - if b == nil { - return nil - } - b.closeOnce.Do(func() { - started := time.Now() - exceeded := false - deadline := time.NewTimer(browserForceTerminationBound) - defer deadline.Stop() - - if b.command != nil && b.command.Process != nil { - b.closeErr = errors.Join(b.closeErr, forceKillBrowserProcess(b.command)) - } - if b.cdp != nil { - b.closeErr = errors.Join(b.closeErr, b.cdp.forceClose()) - } - if b.server != nil { - if err := b.server.Close(); err != nil && !errors.Is(err, http.ErrServerClosed) { - b.closeErr = errors.Join(b.closeErr, err) - } - } - if b.listener != nil { - if err := b.listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) { - b.closeErr = errors.Join(b.closeErr, err) - } - } - if b.process != nil { - select { - case <-b.process.done: - if b.process.err != nil && !browserWasForceKilled(b.process.err) { - b.closeErr = errors.Join(b.closeErr, b.process.err) - } - case <-deadline.C: - exceeded = true - b.closeErr = errors.Join(b.closeErr, fmt.Errorf("force terminate Chromium exceeded %s", browserForceTerminationBound)) - } - } - if b.profile != "" { - profile := b.profile - b.profile = "" - b.closeErr = errors.Join(b.closeErr, os.RemoveAll(profile)) - } - if time.Since(started) > browserForceTerminationBound && !exceeded { - b.closeErr = errors.Join(b.closeErr, fmt.Errorf("force terminate Chromium exceeded %s", browserForceTerminationBound)) - } - }) - return b.closeErr -} - -func browserWasForceKilled(err error) bool { - if err == nil { - return true - } - text := strings.ToLower(err.Error()) - return strings.Contains(text, "signal: killed") || strings.Contains(text, "signal: terminated") || strings.Contains(text, "exit status 137") -} - -func findPageWebsocket(ctx context.Context, browserWebsocket, pageURL string) (string, error) { - parsed, err := url.Parse(browserWebsocket) - if err != nil { - return "", err - } - endpoint := "http://" + parsed.Host + "/json/list" - client := &http.Client{Timeout: 2 * time.Second} - ticker := time.NewTicker(20 * time.Millisecond) - defer ticker.Stop() - for { - request, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) - response, err := client.Do(request) - if err == nil { - var targets []struct { - Type string `json:"type"` - URL string `json:"url"` - WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"` - } - decodeErr := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&targets) - _ = response.Body.Close() - if decodeErr == nil { - for _, target := range targets { - if target.Type == "page" && target.URL == pageURL && strings.HasPrefix(target.WebSocketDebuggerURL, "ws://") { - return target.WebSocketDebuggerURL, nil - } - } - } - } - select { - case <-ctx.Done(): - return "", ctx.Err() - case <-ticker.C: - } - } -} - -func waitBrowserReady(ctx context.Context, client *cdpClient) (browserStatus, error) { - ticker := time.NewTicker(10 * time.Millisecond) - defer ticker.Stop() - for { - raw, err := client.evaluateString(ctx, `typeof window.maltHostStatus === "function" ? window.maltHostStatus() : JSON.stringify({phase:"loading"})`, false) - if err == nil { - var status browserStatus - if err := strictJSON([]byte(raw), &status); err != nil { - return browserStatus{}, err - } - switch status.Phase { - case "ready": - return status, nil - case "failed": - return browserStatus{}, errors.New(status.Error) - } - } - select { - case <-ctx.Done(): - return browserStatus{}, ctx.Err() - case <-ticker.C: - } - } -} - -func scanBrowserLog(reader io.Reader, output *boundedLog, devtools chan<- string) { - buffer := make([]byte, 4096) - pending := "" - found := false - for { - count, err := reader.Read(buffer) - if count > 0 { - chunk := buffer[:count] - _, _ = output.Write(chunk) - if !found { - pending += string(chunk) - if matches := devtoolsPattern.FindStringSubmatch(pending); len(matches) == 2 { - devtools <- matches[1] - found = true - } - if len(pending) > 8192 { - pending = pending[len(pending)-4096:] - } - } - } - if err != nil { - return - } - } -} - -type browserPageConfig struct { - GatewayURL string `json:"gateway_url"` - FixtureURL string `json:"fixture_url"` - WASMURL string `json:"wasm_url"` - WASMSHA256 string `json:"wasm_sha256"` - GatewayToken string `json:"gateway_instance_token"` - Backend string `json:"backend"` -} - -func browserPage(config browserPageConfig) ([]byte, error) { - configuration, err := json.Marshal(config) - if err != nil { - return nil, err - } - quotedConfiguration, err := json.Marshal(string(configuration)) - if err != nil { - return nil, err - } - page := `MALT RQ2 browser writer -` - return []byte(page), nil -} - -type boundedLog struct { - mu sync.Mutex - limit int - buffer bytes.Buffer - truncated bool -} - -func (b *boundedLog) Write(value []byte) (int, error) { - b.mu.Lock() - defer b.mu.Unlock() - original := len(value) - remaining := b.limit - b.buffer.Len() - if remaining > 0 { - _, _ = b.buffer.Write(value[:min(len(value), remaining)]) - } - if original > remaining { - b.truncated = true - } - return original, nil -} - -func (b *boundedLog) String() string { - b.mu.Lock() - defer b.mu.Unlock() - return b.buffer.String() -} - -func formatBrowserLog(log *boundedLog) string { - if log == nil || strings.TrimSpace(log.String()) == "" { - return "" - } - return "; Chromium stderr: " + strings.TrimSpace(log.String()) -} - -func boundedText(value []byte, limit int) string { - value = bytes.TrimSpace(value) - if len(value) > limit { - value = value[:limit] - } - return string(value) -} - -func secondsToNS(value float64) uint64 { - if value <= 0 { - return 0 - } - return uint64(value * float64(time.Second)) -} - -func browserRuntime(engine, wasmSHA string, machineEvidence *machine.Evidence, cold coldStartMetrics) *rq2wire.RuntimeEvidence { - return &rq2wire.RuntimeEvidence{ - OS: runtime.GOOS, Architecture: runtime.GOARCH, LowPowerARM: machineEvidence.Descriptor.LowPowerARM(), - MachineDescriptorID: machineEvidence.Descriptor.ID, MachineDescriptorSHA256: machineEvidence.SHA256, - BrowserEngine: engine, WASMSHA256: wasmSHA, - ParameterProfile: cold.ParameterProfile, ParameterSHA256: cold.ParameterSHA256, ParameterInputBytes: cold.ParameterBytes, - } -} - -func exactCapabilities(backend string) []string { - values := rq2wire.RequiredCapabilities(rq2wire.ClientBrowserWASM, backend) - slices.Sort(values) - return values -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/cdp.go b/tools/evaluation/cmd/malt-eval-rq2-browser-worker/cdp.go deleted file mode 100644 index a9e5ca1..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/cdp.go +++ /dev/null @@ -1,361 +0,0 @@ -package main - -import ( - "bufio" - "context" - "crypto/rand" - "crypto/sha1" // #nosec G505 -- RFC 6455 requires SHA-1 for the handshake accept value. - "encoding/base64" - "encoding/binary" - "encoding/json" - "errors" - "fmt" - "io" - "net" - "net/http" - "net/url" - "strings" - "sync" - "time" -) - -const ( - maxCDPMessageBytes = 16 << 20 - websocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" -) - -type websocketConn struct { - connection net.Conn - reader *bufio.Reader - writeMu sync.Mutex -} - -func dialWebsocket(ctx context.Context, rawURL string) (*websocketConn, error) { - parsed, err := url.Parse(rawURL) - if err != nil || parsed.Scheme != "ws" || parsed.Host == "" { - return nil, fmt.Errorf("invalid local DevTools websocket URL") - } - dialer := net.Dialer{} - connection, err := dialer.DialContext(ctx, "tcp", parsed.Host) - if err != nil { - return nil, err - } - failed := true - defer func() { - if failed { - _ = connection.Close() - } - }() - keyBytes := make([]byte, 16) - if _, err := rand.Read(keyBytes); err != nil { - return nil, err - } - key := base64.StdEncoding.EncodeToString(keyBytes) - path := parsed.EscapedPath() - if path == "" { - path = "/" - } - if parsed.RawQuery != "" { - path += "?" + parsed.RawQuery - } - request := "GET " + path + " HTTP/1.1\r\n" + - "Host: " + parsed.Host + "\r\n" + - "Upgrade: websocket\r\n" + - "Connection: Upgrade\r\n" + - "Sec-WebSocket-Key: " + key + "\r\n" + - "Sec-WebSocket-Version: 13\r\n" + - "Origin: http://127.0.0.1\r\n\r\n" - if _, err := io.WriteString(connection, request); err != nil { - return nil, err - } - reader := bufio.NewReader(connection) - response, err := http.ReadResponse(reader, &http.Request{Method: http.MethodGet}) - if err != nil { - return nil, err - } - acceptDigest := sha1.Sum([]byte(key + websocketGUID)) // #nosec G401 -- required by RFC 6455. - wantAccept := base64.StdEncoding.EncodeToString(acceptDigest[:]) - if response.StatusCode != http.StatusSwitchingProtocols || !strings.EqualFold(response.Header.Get("Upgrade"), "websocket") || response.Header.Get("Sec-WebSocket-Accept") != wantAccept { - _ = response.Body.Close() - return nil, fmt.Errorf("DevTools websocket handshake failed with %s", response.Status) - } - failed = false - return &websocketConn{connection: connection, reader: reader}, nil -} - -func (w *websocketConn) Close() error { - w.writeMu.Lock() - defer w.writeMu.Unlock() - _ = w.writeFrame(0x8, nil) - return w.connection.Close() -} - -func (w *websocketConn) writeText(ctx context.Context, data []byte) error { - if len(data) > maxCDPMessageBytes { - return fmt.Errorf("DevTools command exceeds %d bytes", maxCDPMessageBytes) - } - w.writeMu.Lock() - defer w.writeMu.Unlock() - cancellationDone := make(chan struct{}) - stopCancellation := context.AfterFunc(ctx, func() { - _ = w.connection.SetWriteDeadline(time.Now()) - close(cancellationDone) - }) - defer func() { - if !stopCancellation() { - <-cancellationDone - } - _ = w.connection.SetWriteDeadline(time.Time{}) - }() - if deadline, ok := ctx.Deadline(); ok { - if err := w.connection.SetWriteDeadline(deadline); err != nil { - return err - } - } - if err := w.writeFrame(0x1, data); err != nil { - if ctx.Err() != nil { - return ctx.Err() - } - return err - } - return nil -} - -func (w *websocketConn) writeFrame(opcode byte, data []byte) error { - header := []byte{0x80 | opcode} - switch { - case len(data) < 126: - header = append(header, 0x80|byte(len(data))) - case uint64(len(data)) <= uint64(^uint16(0)): - header = append(header, 0x80|126, 0, 0) - binary.BigEndian.PutUint16(header[len(header)-2:], uint16(len(data))) - default: - header = append(header, 0x80|127, 0, 0, 0, 0, 0, 0, 0, 0) - binary.BigEndian.PutUint64(header[len(header)-8:], uint64(len(data))) - } - mask := make([]byte, 4) - if _, err := rand.Read(mask); err != nil { - return err - } - header = append(header, mask...) - masked := make([]byte, len(data)) - for index := range data { - masked[index] = data[index] ^ mask[index%len(mask)] - } - if _, err := w.connection.Write(header); err != nil { - return err - } - _, err := w.connection.Write(masked) - return err -} - -func (w *websocketConn) readText(ctx context.Context) ([]byte, error) { - cancellationDone := make(chan struct{}) - stopCancellation := context.AfterFunc(ctx, func() { - _ = w.connection.SetReadDeadline(time.Now()) - close(cancellationDone) - }) - defer func() { - if !stopCancellation() { - <-cancellationDone - } - _ = w.connection.SetReadDeadline(time.Time{}) - }() - if deadline, ok := ctx.Deadline(); ok { - if err := w.connection.SetReadDeadline(deadline); err != nil { - return nil, err - } - } - var assembled []byte - continuation := false - for { - first, err := w.reader.ReadByte() - if err != nil { - if ctx.Err() != nil { - return nil, ctx.Err() - } - return nil, err - } - second, err := w.reader.ReadByte() - if err != nil { - if ctx.Err() != nil { - return nil, ctx.Err() - } - return nil, err - } - final := first&0x80 != 0 - opcode := first & 0x0f - if first&0x70 != 0 || second&0x80 != 0 { - return nil, fmt.Errorf("invalid DevTools websocket frame") - } - length := uint64(second & 0x7f) - switch length { - case 126: - var raw [2]byte - if _, err := io.ReadFull(w.reader, raw[:]); err != nil { - return nil, err - } - length = uint64(binary.BigEndian.Uint16(raw[:])) - case 127: - var raw [8]byte - if _, err := io.ReadFull(w.reader, raw[:]); err != nil { - return nil, err - } - length = binary.BigEndian.Uint64(raw[:]) - } - if length > maxCDPMessageBytes || uint64(len(assembled))+length > maxCDPMessageBytes { - return nil, fmt.Errorf("DevTools websocket message exceeds %d bytes", maxCDPMessageBytes) - } - payload := make([]byte, int(length)) - if _, err := io.ReadFull(w.reader, payload); err != nil { - return nil, err - } - switch opcode { - case 0x8: - return nil, io.EOF - case 0x9: - w.writeMu.Lock() - err := w.writeFrame(0xA, payload) - w.writeMu.Unlock() - if err != nil { - return nil, err - } - continue - case 0xA: - continue - case 0x1: - if continuation { - return nil, fmt.Errorf("unexpected DevTools text frame") - } - assembled = append(assembled, payload...) - continuation = !final - case 0x0: - if !continuation { - return nil, fmt.Errorf("unexpected DevTools continuation frame") - } - assembled = append(assembled, payload...) - continuation = !final - default: - return nil, fmt.Errorf("unsupported DevTools websocket opcode %d", opcode) - } - if final { - return assembled, nil - } - } -} - -type cdpClient struct { - websocket *websocketConn - nextID uint64 -} - -type cdpResponse struct { - ID uint64 `json:"id"` - Result json.RawMessage `json:"result"` - Error *struct { - Code int `json:"code"` - Message string `json:"message"` - } `json:"error"` -} - -func newCDPClient(ctx context.Context, websocketURL string) (*cdpClient, error) { - connection, err := dialWebsocket(ctx, websocketURL) - if err != nil { - return nil, err - } - return &cdpClient{websocket: connection}, nil -} - -func (c *cdpClient) Close() error { return c.websocket.Close() } - -// forceClose skips the websocket close handshake and write mutex. It is used -// only after the owning Chromium process has received a non-graceful kill, so -// a stuck writer cannot delay fail-closed session termination. -func (c *cdpClient) forceClose() error { return c.websocket.connection.Close() } - -func (c *cdpClient) call(ctx context.Context, method string, params any, result any) error { - c.nextID++ - id := c.nextID - command, err := json.Marshal(struct { - ID uint64 `json:"id"` - Method string `json:"method"` - Params any `json:"params,omitempty"` - }{ID: id, Method: method, Params: params}) - if err != nil { - return err - } - if err := c.websocket.writeText(ctx, command); err != nil { - return err - } - for { - message, err := c.websocket.readText(ctx) - if err != nil { - return err - } - var response cdpResponse - if err := json.Unmarshal(message, &response); err != nil { - return err - } - if response.ID != id { - continue - } - if response.Error != nil { - return fmt.Errorf("DevTools %s failed (%d): %s", method, response.Error.Code, response.Error.Message) - } - if result == nil { - return nil - } - if len(response.Result) == 0 { - return errors.New("DevTools response omitted result") - } - return json.Unmarshal(response.Result, result) - } -} - -type runtimeEvaluateResult struct { - Result struct { - Type string `json:"type"` - Value json.RawMessage `json:"value"` - Description string `json:"description"` - } `json:"result"` - ExceptionDetails json.RawMessage `json:"exceptionDetails"` -} - -func (c *cdpClient) evaluateString(ctx context.Context, expression string, await bool) (string, error) { - var result runtimeEvaluateResult - err := c.call(ctx, "Runtime.evaluate", map[string]any{ - "expression": expression, "awaitPromise": await, "returnByValue": true, - }, &result) - if err != nil { - return "", err - } - if len(result.ExceptionDetails) != 0 { - return "", fmt.Errorf("browser JavaScript exception: %s", string(result.ExceptionDetails)) - } - if result.Result.Type != "string" { - return "", fmt.Errorf("browser expression returned %q, want string", result.Result.Type) - } - var value string - if err := json.Unmarshal(result.Result.Value, &value); err != nil { - return "", err - } - return value, nil -} - -func (c *cdpClient) taskDuration(ctx context.Context) (float64, error) { - var response struct { - Metrics []struct { - Name string `json:"name"` - Value float64 `json:"value"` - } `json:"metrics"` - } - if err := c.call(ctx, "Performance.getMetrics", map[string]any{}, &response); err != nil { - return 0, err - } - for _, metric := range response.Metrics { - if metric.Name == "TaskDuration" { - return metric.Value, nil - } - } - return 0, fmt.Errorf("Chromium omitted Performance.TaskDuration") -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/cdp_test.go b/tools/evaluation/cmd/malt-eval-rq2-browser-worker/cdp_test.go deleted file mode 100644 index 556cd53..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/cdp_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "bufio" - "context" - "errors" - "net" - "testing" - "time" -) - -func TestWebsocketReadCancellationIsImmediateAndDoesNotPoisonNextCall(t *testing.T) { - client, server := net.Pipe() - t.Cleanup(func() { - _ = client.Close() - _ = server.Close() - }) - websocket := &websocketConn{connection: client, reader: bufio.NewReader(client)} - ctx, cancel := context.WithCancel(context.Background()) - result := make(chan error, 1) - go func() { - _, err := websocket.readText(ctx) - result <- err - }() - cancel() - select { - case err := <-result: - if !errors.Is(err, context.Canceled) { - t.Fatalf("canceled websocket read = %v", err) - } - case <-time.After(time.Second): - t.Fatal("context cancellation did not interrupt websocket read") - } - - go func() { - _, _ = server.Write([]byte{0x81, 0x02, 'o', 'k'}) - }() - next, err := websocket.readText(context.Background()) - if err != nil || string(next) != "ok" { - t.Fatalf("read after cancellation = %q, %v", next, err) - } -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/main.go b/tools/evaluation/cmd/malt-eval-rq2-browser-worker/main.go deleted file mode 100644 index 3396c91..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/main.go +++ /dev/null @@ -1,571 +0,0 @@ -// Command malt-eval-rq2-browser-worker is the real-browser process boundary -// for paper RQ2. It launches a pinned Chromium-family executable, serves the -// pinned Go/WASM writer through a non-cacheable loopback origin, and relays the -// evaluator's strict JSONL protocol through DevTools. Stdout contains JSONL -// records only; browser diagnostics are bounded on stderr. -package main - -import ( - "bufio" - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "net/url" - "os" - "regexp" - "runtime" - "slices" - "strconv" - "strings" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/machine" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2wire" -) - -const ( - maxWorkerLineBytes = 1 << 20 - maxWorkerRequests = 2048 - maxBrowserLauncherBytes = 512 << 20 -) - -var browserEnginePattern = regexp.MustCompile(`^(chromium|chrome)-[0-9]+(?:\.[0-9]+){1,3}$`) - -type stringList []string - -func (s *stringList) String() string { return strings.Join(*s, ",") } -func (s *stringList) Set(value string) error { - if value == "" || strings.HasPrefix(value, "-") || strings.IndexByte(value, 0) >= 0 || len(value) > 4096 || len(*s) >= 16 { - return fmt.Errorf("invalid or excessive browser launch argument") - } - *s = append(*s, value) - return nil -} - -type hostConfig struct { - gatewayBaseURL string - gatewayToken string - fixturePath string - workerID string - platformID string - clientKind string - backend string - lifecycle string - steadyWarmup string - sessionMutationCount int - requestTimeout time.Duration - lowPowerARM bool - browserPath string - browserEngine string - browserArgs []string - browserPin artifactPin - wasmPin artifactPin - wasmExecPin artifactPin - machinePin artifactPin -} - -type browserWorker struct { - config hostConfig - fixture []byte - fixtureSHA256 string - wasm []byte - wasmExec []byte - browserArtifact artifactSnapshot - wasmArtifact artifactSnapshot - wasmExecArtifact artifactSnapshot - capabilityError error - browser *browserSession - sessionID string - fixtureID string - state string - preflightComplete bool - mutationCount uint64 - measuredMutationCount uint64 - machine *machine.Evidence -} - -func main() { - if len(os.Args) > 1 && os.Args[1] == "--e0-self-test" { - if err := runBrowserE0SelfTest(os.Args[2:], os.Stdout, os.Stderr); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - return - } - if err := run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func run(args []string, stdin io.Reader, stdout, stderr io.Writer) (returnErr error) { - config, err := parseFlags(args, stderr) - if err != nil { - return err - } - worker, err := newBrowserWorker(config) - if err != nil { - return err - } - defer func() { - if worker.browser != nil { - returnErr = errors.Join(returnErr, worker.browser.close()) - } - }() - encoder := json.NewEncoder(stdout) - encoder.SetEscapeHTML(false) - scanner := bufio.NewScanner(stdin) - scanner.Buffer(make([]byte, 64<<10), maxWorkerLineBytes) - requests := 0 - for scanner.Scan() { - requests++ - if requests > maxWorkerRequests { - return fmt.Errorf("RQ2 browser worker exceeds %d-request limit", maxWorkerRequests) - } - request, err := decodeWorkerRequest(scanner.Bytes()) - if err != nil { - return err - } - if err := worker.bindRequest(request); err != nil { - return err - } - requestContext, cancel := context.WithTimeout(context.Background(), config.requestTimeout) - record := worker.exchange(requestContext, request) - cancel() - if err := rq2wire.Bind(record, request); err != nil { - return err - } - if err := record.Validate(); err != nil { - return fmt.Errorf("browser worker constructed invalid record: %w", err) - } - if err := encoder.Encode(record); err != nil { - return err - } - if request.RecordKind == rq2wire.RecordSessionEnd { - if scanner.Scan() { - return fmt.Errorf("RQ2 browser worker received input after session-end") - } - if err := scanner.Err(); err != nil { - return err - } - if record.Success { - return worker.verifyArtifacts() - } - return nil - } - } - if err := scanner.Err(); err != nil { - if strings.Contains(err.Error(), "token too long") { - return fmt.Errorf("RQ2 browser worker request exceeds %d bytes", maxWorkerLineBytes) - } - return err - } - return fmt.Errorf("RQ2 browser worker input closed before session-end") -} - -func parseFlags(args []string, stderr io.Writer) (hostConfig, error) { - flags := flag.NewFlagSet("malt-eval-rq2-browser-worker", flag.ContinueOnError) - flags.SetOutput(stderr) - baseURL := flags.String("gateway-base-url", "", "Gateway base URL") - gatewayToken := flags.String("gateway-instance-token", "", "disposable Gateway token") - fixture := flags.String("fixture", "", "pinned logical fixture") - workerID := flags.String("worker-id", "", "registered worker identifier") - platformID := flags.String("platform-id", "", "registered platform identifier") - clientKind := flags.String("client-kind", "", "must be browser-wasm") - backend := flags.String("backend", "", "kzg or ipa") - lifecycle := flags.String("lifecycle", "", "browser-cold or browser-steady") - steadyWarmup := flags.String("steady-warmup-operation", "", "exact unmeasured steady-session warmup operation") - sessionMutationCount := flags.Int("session-mutation-count", 0, "exact measured mutation count for a short-session pair") - lowPower := flags.String("low-power-arm", "", "registered low-power ARM classification") - timeout := flags.Duration("request-timeout", 5*time.Minute, "per-request timeout") - browserPath := flags.String("browser-path", "", "pinned Chromium-family launcher") - browserEngine := flags.String("browser-engine", "", "exact detected browser engine/version") - browserSHA := flags.String("browser-launcher-sha256", "", "launcher SHA-256") - browserBytes := flags.Int64("browser-launcher-bytes", 0, "launcher bytes") - wasmPath := flags.String("wasm", "", "Go/WASM writer artifact") - wasmSHA := flags.String("wasm-sha256", "", "Go/WASM writer SHA-256") - wasmBytes := flags.Int64("wasm-bytes", 0, "Go/WASM writer bytes") - wasmExecPath := flags.String("wasm-exec", "", "Go wasm_exec.js artifact") - wasmExecSHA := flags.String("wasm-exec-sha256", "", "wasm_exec.js SHA-256") - wasmExecBytes := flags.Int64("wasm-exec-bytes", 0, "wasm_exec.js bytes") - machinePath := flags.String("machine-descriptor", "", "pinned machine descriptor artifact") - machineSHA := flags.String("machine-descriptor-sha256", "", "machine descriptor SHA-256") - machineBytes := flags.Int64("machine-descriptor-bytes", 0, "machine descriptor bytes") - var browserArgs stringList - flags.Var(&browserArgs, "browser-launch-arg", "argument before Chromium flags (repeatable)") - if err := flags.Parse(args); err != nil { - return hostConfig{}, err - } - if flags.NArg() != 0 { - return hostConfig{}, fmt.Errorf("unexpected positional arguments") - } - if *lowPower != "true" && *lowPower != "false" { - return hostConfig{}, fmt.Errorf("-low-power-arm must be explicitly true or false") - } - lowPowerARM, _ := strconv.ParseBool(*lowPower) - config := hostConfig{ - gatewayBaseURL: strings.TrimRight(strings.TrimSpace(*baseURL), "/"), gatewayToken: strings.TrimSpace(*gatewayToken), - fixturePath: strings.TrimSpace(*fixture), workerID: strings.TrimSpace(*workerID), platformID: strings.TrimSpace(*platformID), - clientKind: strings.TrimSpace(*clientKind), backend: strings.TrimSpace(*backend), lifecycle: strings.TrimSpace(*lifecycle), - steadyWarmup: strings.TrimSpace(*steadyWarmup), - sessionMutationCount: *sessionMutationCount, - requestTimeout: *timeout, lowPowerARM: lowPowerARM, browserPath: strings.TrimSpace(*browserPath), - browserEngine: strings.TrimSpace(*browserEngine), browserArgs: slices.Clone(browserArgs), - } - config.browserPin = artifactPin{path: config.browserPath, sha256: strings.TrimSpace(*browserSHA), bytes: *browserBytes, executable: true, allowSymlink: true, maxBytes: maxBrowserLauncherBytes} - config.wasmPin = artifactPin{path: strings.TrimSpace(*wasmPath), sha256: strings.TrimSpace(*wasmSHA), bytes: *wasmBytes, maxBytes: maxWASMBytes} - config.wasmExecPin = artifactPin{path: strings.TrimSpace(*wasmExecPath), sha256: strings.TrimSpace(*wasmExecSHA), bytes: *wasmExecBytes, maxBytes: maxSupportBytes} - config.machinePin = artifactPin{path: strings.TrimSpace(*machinePath), sha256: strings.TrimSpace(*machineSHA), bytes: *machineBytes, maxBytes: machine.MaxDescriptorBytes} - parsedURL, err := url.Parse(config.gatewayBaseURL) - if err != nil || parsedURL.Scheme != "http" && parsedURL.Scheme != "https" || parsedURL.Host == "" || parsedURL.RawQuery != "" || parsedURL.Fragment != "" { - return hostConfig{}, fmt.Errorf("invalid -gateway-base-url") - } - if !rq2wire.IDPattern.MatchString(config.workerID) || !rq2wire.IDPattern.MatchString(config.platformID) || !canonicalSHA256(config.gatewayToken) || config.fixturePath == "" { - return hostConfig{}, fmt.Errorf("worker/platform/fixture/Gateway token flags are incomplete") - } - if config.clientKind != rq2wire.ClientBrowserWASM || config.backend != "kzg" && config.backend != "ipa" || - config.lifecycle != rq2wire.LifecycleBrowserCold && config.lifecycle != rq2wire.LifecycleBrowserSteady && config.lifecycle != rq2wire.LifecycleBrowserShort { - return hostConfig{}, fmt.Errorf("browser worker coordinate is invalid") - } - if config.sessionMutationCount < 0 || config.sessionMutationCount > 1024 || - (config.sessionMutationCount > 0 && !slices.Contains([]int{1, 2, 4, 16, 64}, config.sessionMutationCount)) || - (config.lifecycle == rq2wire.LifecycleBrowserShort && config.sessionMutationCount == 0) || - (config.lifecycle == rq2wire.LifecycleBrowserCold && config.sessionMutationCount != 0) { - return hostConfig{}, fmt.Errorf("browser worker session mutation count is invalid") - } - if !browserEnginePattern.MatchString(config.browserEngine) || !rq2wire.IDPattern.MatchString(config.steadyWarmup) || - config.requestTimeout <= 0 || config.requestTimeout > 24*time.Hour { - return hostConfig{}, fmt.Errorf("browser engine or request timeout is invalid") - } - if config.lowPowerARM && runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" { - return hostConfig{}, fmt.Errorf("low-power ARM cannot be claimed by %s", runtime.GOARCH) - } - for _, pin := range []artifactPin{config.browserPin, config.wasmPin, config.wasmExecPin} { - if pin.path == "" || !canonicalSHA256(pin.sha256) || pin.bytes <= 0 { - return hostConfig{}, fmt.Errorf("browser/WASM artifact pins are required") - } - } - if config.machinePin.path == "" || !canonicalSHA256(config.machinePin.sha256) || config.machinePin.bytes <= 0 || config.machinePin.bytes > machine.MaxDescriptorBytes { - return hostConfig{}, fmt.Errorf("machine descriptor path/SHA-256/bytes pin is required") - } - return config, nil -} - -func newBrowserWorker(config hostConfig) (*browserWorker, error) { - fixture, err := readFixture(config.fixturePath) - if err != nil { - return nil, err - } - machineEvidence, err := machine.LoadPinned(config.machinePin.path, config.machinePin.sha256, config.machinePin.bytes) - if err != nil { - return nil, fmt.Errorf("verify paper machine descriptor: %w", err) - } - if machineEvidence.Descriptor.LowPowerARM() != config.lowPowerARM { - return nil, fmt.Errorf("-low-power-arm does not match the verified machine descriptor classification") - } - wasmArtifact, wasm, err := loadPinnedArtifact(config.wasmPin, true) - if err != nil { - return nil, fmt.Errorf("verify Go/WASM writer: %w", err) - } - wasmExecArtifact, wasmExec, err := loadPinnedArtifact(config.wasmExecPin, true) - if err != nil { - return nil, fmt.Errorf("verify wasm_exec.js: %w", err) - } - worker := &browserWorker{ - config: config, fixture: fixture, wasm: wasm, wasmExec: wasmExec, - wasmArtifact: wasmArtifact, wasmExecArtifact: wasmExecArtifact, state: rq2wire.RecordPreflight, machine: machineEvidence, - } - digest := sha256.Sum256(fixture) - worker.fixtureSHA256 = hex.EncodeToString(digest[:]) - browserArtifact, _, err := loadPinnedArtifact(config.browserPin, false) - if err != nil { - worker.capabilityError = fmt.Errorf("pinned browser launcher unavailable: %w", err) - } else if runtime.GOOS != "linux" { - worker.capabilityError = fmt.Errorf("real browser peak-RSS measurement requires Linux /proc") - } else { - worker.browserArtifact = browserArtifact - } - return worker, nil -} - -func (w *browserWorker) bindRequest(request rq2wire.WorkerRequest) error { - if request.ClientKind != w.config.clientKind || request.Backend != w.config.backend || request.Lifecycle != w.config.lifecycle || - request.WorkerID != w.config.workerID || request.PlatformID != w.config.platformID { - return fmt.Errorf("RQ2 browser request does not bind configured worker coordinate") - } - if w.sessionID == "" { - w.sessionID, w.fixtureID = request.SessionID, request.FixtureID - } - if request.SessionID != w.sessionID || request.FixtureID != w.fixtureID { - return fmt.Errorf("RQ2 browser request changed session or fixture identity") - } - switch w.state { - case rq2wire.RecordPreflight, rq2wire.RecordSessionStart: - if request.RecordKind != w.state { - return fmt.Errorf("RQ2 browser request kind %q is out of order; want %q", request.RecordKind, w.state) - } - case "active": - if request.RecordKind != rq2wire.RecordMutation && request.RecordKind != rq2wire.RecordSessionEnd { - return fmt.Errorf("RQ2 browser active session rejects %q", request.RecordKind) - } - default: - return fmt.Errorf("RQ2 browser worker is closed") - } - return nil -} - -func (w *browserWorker) exchange(ctx context.Context, request rq2wire.WorkerRequest) rq2wire.WorkerRecord { - if request.RecordKind == rq2wire.RecordPreflight { - return w.preflight(ctx, request) - } - if w.browser == nil { - return rq2wire.FailedRecord(request, "capability_unavailable", fmt.Errorf("real browser session is unavailable")) - } - if request.RecordKind == rq2wire.RecordMutation { - if w.config.lifecycle == rq2wire.LifecycleBrowserCold && (w.mutationCount != 0 || !request.Measured) { - return rq2wire.FailedRecord(request, "lifecycle_invalid", fmt.Errorf("cold browser lifecycle requires exactly one measured first mutation")) - } - if w.config.lifecycle == rq2wire.LifecycleBrowserShort && (!request.Measured || w.mutationCount >= uint64(w.config.sessionMutationCount)) { - return rq2wire.FailedRecord(request, "lifecycle_invalid", fmt.Errorf("short browser lifecycle requires exactly %d measured mutations", w.config.sessionMutationCount)) - } - if w.config.lifecycle == rq2wire.LifecycleBrowserSteady { - if w.mutationCount == 0 && (request.Measured || request.Operation != w.config.steadyWarmup) { - return rq2wire.FailedRecord(request, "lifecycle_invalid", fmt.Errorf("steady browser lifecycle must begin with exact unmeasured warmup %q", w.config.steadyWarmup)) - } - if w.mutationCount != 0 && !request.Measured { - return rq2wire.FailedRecord(request, "lifecycle_invalid", fmt.Errorf("steady browser lifecycle permits only one unmeasured warmup")) - } - if w.config.sessionMutationCount > 0 && w.mutationCount > uint64(w.config.sessionMutationCount) { - return rq2wire.FailedRecord(request, "lifecycle_invalid", fmt.Errorf("paired steady baseline permits exactly %d measured mutations", w.config.sessionMutationCount)) - } - } - } - if request.RecordKind == rq2wire.RecordSessionEnd { - if (w.config.lifecycle == rq2wire.LifecycleBrowserCold && w.mutationCount != 1) || - (w.config.lifecycle == rq2wire.LifecycleBrowserShort && (w.mutationCount != uint64(w.config.sessionMutationCount) || w.measuredMutationCount != uint64(w.config.sessionMutationCount))) || - (w.config.lifecycle == rq2wire.LifecycleBrowserSteady && w.config.sessionMutationCount == 0 && w.mutationCount < 2) || - (w.config.lifecycle == rq2wire.LifecycleBrowserSteady && w.config.sessionMutationCount > 0 && (w.mutationCount != uint64(w.config.sessionMutationCount+1) || w.measuredMutationCount != uint64(w.config.sessionMutationCount))) { - return rq2wire.FailedRecord(request, "lifecycle_invalid", fmt.Errorf("browser session ended before its cold/steady mutation contract completed")) - } - } - record, call, cpuNS, peakBytes, samples, err := w.browser.exchange(ctx, request) - if err != nil { - return rq2wire.FailedRecord(request, "browser_execution", w.poisonBrowser(err)) - } - if err := rq2wire.Bind(record, request); err != nil { - return rq2wire.FailedRecord(request, "browser_protocol", w.poisonBrowser(err)) - } - if record.Success && record.RecordKind == rq2wire.RecordMutation { - metrics := &record.Mutation.Metrics - metrics.CPUTotal = measuredPhase(cpuNS, 0, 1) - metrics.PeakMemory = measuredPhase(call.CallNS, peakBytes, samples) - requestJSON, _ := json.Marshal(request) - metrics.JSWASMBoundary = measuredPhase(call.BoundaryNS, uint64(len(requestJSON)+len(call.RecordJSON)), 1) - if w.config.lifecycle == rq2wire.LifecycleBrowserCold || w.config.lifecycle == rq2wire.LifecycleBrowserShort && w.mutationCount == 0 { - metrics.WASMDownload = measuredPhase(w.browser.cold.WASMDownloadNS, w.browser.cold.WASMDownloadBytes, 1) - metrics.WASMInstantiate = measuredPhase(w.browser.cold.WASMInstantiateNS, 0, 1) - metrics.ParameterLoad = measuredPhase(w.browser.cold.ParameterLoadNS, w.browser.cold.ParameterBytes, 1) - metrics.FirstMutation = measuredPhase(call.CallNS, 0, 1) - } - } - if record.Success { - switch request.RecordKind { - case rq2wire.RecordSessionStart: - w.state = "active" - case rq2wire.RecordMutation: - w.mutationCount++ - if request.Measured { - w.measuredMutationCount++ - } - case rq2wire.RecordSessionEnd: - if err := w.verifyArtifacts(); err != nil { - return rq2wire.FailedRecord(request, "artifact_changed", err) - } - w.state = "closed" - } - } - return record -} - -// poisonBrowser closes the process boundary after an exchange timeout or an -// unbindable result. The Go/WASM request uses browser-owned fetches which may -// outlive the CDP wait context; terminating Chromium is the only fail-closed -// guarantee that no late request can mutate the disposable Gateway or overlap -// a later sample. -func (w *browserWorker) poisonBrowser(cause error) error { - if cause == nil { - cause = errors.New("browser session entered an unknown terminal state") - } - browser := w.browser - w.browser = nil - w.state = "poisoned" - if browser == nil { - return cause - } - return errors.Join(cause, browser.forceTerminate()) -} - -func (w *browserWorker) preflight(_ context.Context, request rq2wire.WorkerRequest) rq2wire.WorkerRecord { - if w.preflightComplete { - return rq2wire.FailedRecord(request, "session_state", fmt.Errorf("browser preflight was already attempted")) - } - w.preflightComplete = true - if w.capabilityError != nil { - return rq2wire.FailedRecord(request, "capability_unavailable", w.capabilityError) - } - if err := w.verifyArtifacts(); err != nil { - return rq2wire.FailedRecord(request, "artifact_changed", err) - } - // The outer JSONL loop cancels each request context after emitting its - // record. Chromium is a session-lifetime process, so bind startup to its - // own bounded context inside startBrowser and close it explicitly when the - // worker session ends instead of killing it after preflight. - browser, err := startBrowser(context.Background(), browserLaunchConfig{ - path: w.config.browserPath, arguments: w.config.browserArgs, engine: w.config.browserEngine, - timeout: w.config.requestTimeout, gatewayURL: w.config.gatewayBaseURL, token: w.config.gatewayToken, - backend: w.config.backend, wasmSHA256: w.config.wasmPin.sha256, - }, w.fixture, w.wasm, w.wasmExec) - if err != nil { - return rq2wire.FailedRecord(request, "capability_unavailable", err) - } - w.browser = browser - record := rq2wire.BaseRecord(request) - record.Success = true - record.Capabilities = exactCapabilities(w.config.backend) - record.Runtime = browserRuntime(browser.engine, w.config.wasmPin.sha256, w.machine, browser.cold) - w.state = rq2wire.RecordSessionStart - return record -} - -func (w *browserWorker) verifyArtifacts() error { - if w.capabilityError == nil { - if err := w.browserArtifact.verify(); err != nil { - return fmt.Errorf("browser launcher: %w", err) - } - } - if err := w.wasmArtifact.verify(); err != nil { - return fmt.Errorf("Go/WASM writer: %w", err) - } - if err := w.wasmExecArtifact.verify(); err != nil { - return fmt.Errorf("wasm_exec.js: %w", err) - } - machineEvidence, err := machine.LoadPinned(w.config.machinePin.path, w.config.machinePin.sha256, w.config.machinePin.bytes) - if err != nil || w.machine == nil || machineEvidence.Descriptor.ID != w.machine.Descriptor.ID || machineEvidence.SHA256 != w.machine.SHA256 { - if err == nil { - err = fmt.Errorf("machine descriptor identity changed") - } - return fmt.Errorf("machine descriptor: %w", err) - } - fixture, err := readFixture(w.config.fixturePath) - if err != nil { - return err - } - digest := sha256.Sum256(fixture) - if len(fixture) != len(w.fixture) || hex.EncodeToString(digest[:]) != w.fixtureSHA256 { - return fmt.Errorf("browser fixture changed after startup") - } - return nil -} - -func measuredPhase(duration, bytes, count uint64) rq2wire.PhaseMeasurement { - return rq2wire.ObservedPhase(duration, bytes, max(uint64(1), count)) -} - -func decodeWorkerRequest(raw []byte) (rq2wire.WorkerRequest, error) { - var request rq2wire.WorkerRequest - if err := strictJSON(raw, &request); err != nil { - return rq2wire.WorkerRequest{}, fmt.Errorf("decode RQ2 browser request: %w", err) - } - if err := request.Validate(); err != nil { - return rq2wire.WorkerRequest{}, err - } - return request, nil -} - -func strictJSON(raw []byte, target any) error { - if err := rejectDuplicateJSONKeys(raw); err != nil { - return err - } - decoder := json.NewDecoder(bytes.NewReader(raw)) - decoder.DisallowUnknownFields() - if err := decoder.Decode(target); err != nil { - return err - } - if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { - return fmt.Errorf("JSON must contain exactly one value") - } - return nil -} - -func rejectDuplicateJSONKeys(raw []byte) error { - decoder := json.NewDecoder(bytes.NewReader(raw)) - decoder.UseNumber() - if err := scanJSONValue(decoder, "$"); err != nil { - return err - } - if _, err := decoder.Token(); !errors.Is(err, io.EOF) { - if err == nil { - return fmt.Errorf("JSON contains trailing data") - } - return err - } - return nil -} - -func scanJSONValue(decoder *json.Decoder, path string) error { - token, err := decoder.Token() - if err != nil { - return err - } - delimiter, ok := token.(json.Delim) - if !ok { - return nil - } - switch delimiter { - case '{': - seen := map[string]struct{}{} - for decoder.More() { - keyToken, err := decoder.Token() - if err != nil { - return err - } - key, ok := keyToken.(string) - if !ok { - return fmt.Errorf("non-string object key at %s", path) - } - if _, exists := seen[key]; exists { - return fmt.Errorf("duplicate object key %q at %s", key, path) - } - seen[key] = struct{}{} - if err := scanJSONValue(decoder, path+"."+key); err != nil { - return err - } - } - _, err := decoder.Token() - return err - case '[': - for index := 0; decoder.More(); index++ { - if err := scanJSONValue(decoder, fmt.Sprintf("%s[%d]", path, index)); err != nil { - return err - } - } - _, err := decoder.Token() - return err - default: - return fmt.Errorf("unexpected JSON delimiter %q at %s", delimiter, path) - } -} - -func canonicalSHA256(value string) bool { - if len(value) != 64 { - return false - } - decoded, err := hex.DecodeString(value) - return err == nil && hex.EncodeToString(decoded) == value -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/main_test.go b/tools/evaluation/cmd/malt-eval-rq2-browser-worker/main_test.go deleted file mode 100644 index c3517a6..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/main_test.go +++ /dev/null @@ -1,480 +0,0 @@ -package main - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" - "strings" - "testing" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/machine" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2e0" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2wire" - cid "github.com/ipfs/go-cid" -) - -func TestParseBrowserEngineOutputUsesExactVersion(t *testing.T) { - for input, expected := range map[string]string{ - "Chromium 150.0.7871.114 snap\n": "chromium-150.0.7871.114", - "Google Chrome 149.0.7654.2": "chrome-149.0.7654.2", - "Google Chrome for Testing 148.0.1.2\n": "chrome-148.0.1.2", - } { - actual, err := parseBrowserEngineOutput(input) - if err != nil || actual != expected { - t.Fatalf("parseBrowserEngineOutput(%q) = %q, %v; want %q", input, actual, err, expected) - } - } - for _, invalid := range []string{"Chromium", "Firefox 150.0", "Chromium 150", "Chromium latest"} { - if _, err := parseBrowserEngineOutput(invalid); err == nil { - t.Fatalf("invalid version output %q was accepted", invalid) - } - } -} - -func TestPoisonBrowserMakesSessionTerminal(t *testing.T) { - worker := &browserWorker{browser: &browserSession{}, state: "active"} - cause := fmt.Errorf("exchange timeout") - if err := worker.poisonBrowser(cause); !strings.Contains(err.Error(), cause.Error()) { - t.Fatalf("poison error = %v", err) - } - if worker.browser != nil || worker.state != "poisoned" { - t.Fatalf("poisoned worker retained browser/state: %#v", worker) - } - request := rq2wire.WorkerRequest{RecordKind: rq2wire.RecordMutation} - if err := worker.bindRequest(request); err == nil || !strings.Contains(err.Error(), "closed") { - t.Fatalf("poisoned worker accepted another request: %v", err) - } -} - -func TestPoisonBrowserImmediatelyForceTerminatesProcess(t *testing.T) { - command := exec.Command(os.Args[0], "-test.run=TestBrowserForceTerminationHelper") - command.Env = append(os.Environ(), "MALT_BROWSER_FORCE_TERMINATION_HELPER=1") - configureBrowserProcess(command) - if err := command.Start(); err != nil { - t.Fatal(err) - } - completion := &processCompletion{done: make(chan struct{})} - go func() { - completion.err = command.Wait() - close(completion.done) - }() - worker := &browserWorker{browser: &browserSession{command: command, process: completion}, state: "active"} - started := time.Now() - err := worker.poisonBrowser(errors.New("forced test failure")) - if elapsed := time.Since(started); elapsed > browserForceTerminationBound+time.Second { - t.Fatalf("force termination took %s, bound %s", elapsed, browserForceTerminationBound) - } - if err == nil || !strings.Contains(err.Error(), "forced test failure") || strings.Contains(err.Error(), "exceeded") { - t.Fatalf("force termination error = %v", err) - } - select { - case <-completion.done: - default: - t.Fatal("force termination returned before the browser process was reaped") - } -} - -func TestBrowserForceTerminationHelper(t *testing.T) { - if os.Getenv("MALT_BROWSER_FORCE_TERMINATION_HELPER") != "1" { - return - } - for { - time.Sleep(time.Hour) - } -} - -func TestParseFlagsRequiresPinnedBrowserAndWASMArtifacts(t *testing.T) { - digest := strings.Repeat("a", 64) - base := []string{ - "-gateway-base-url", "http://127.0.0.1:8080", "-gateway-instance-token", digest, - "-fixture", "/tmp/fixture", "-worker-id", "worker", "-platform-id", "browser", - "-client-kind", "browser-wasm", "-backend", "kzg", "-lifecycle", "browser-cold", - "-steady-warmup-operation", "map-replace", - "-low-power-arm=false", "-browser-path", "/usr/bin/chromium", "-browser-engine", "chromium-150.0.7871.114", - "-browser-launcher-sha256", digest, "-browser-launcher-bytes", "1", - "-wasm", "/tmp/writer.wasm", "-wasm-sha256", digest, "-wasm-bytes", "1", - "-wasm-exec", "/tmp/wasm_exec.js", "-wasm-exec-sha256", digest, "-wasm-exec-bytes", "1", - "-machine-descriptor", "/tmp/machine.json", "-machine-descriptor-sha256", digest, "-machine-descriptor-bytes", "1", - } - config, err := parseFlags(base, &bytes.Buffer{}) - if err != nil || config.clientKind != rq2wire.ClientBrowserWASM { - t.Fatalf("parseFlags() = %#v, %v", config, err) - } - for index, value := range base { - if value == "-wasm-sha256" { - hostile := append([]string(nil), base...) - hostile[index+1] = "latest" - if _, err := parseFlags(hostile, &bytes.Buffer{}); err == nil { - t.Fatal("non-pinned WASM artifact was accepted") - } - break - } - } - short := append([]string(nil), base...) - for index := range short { - if short[index] == "browser-cold" { - short[index] = "browser-short-session" - break - } - } - if _, err := parseFlags(short, &bytes.Buffer{}); err == nil { - t.Fatal("short-session lifecycle without an exact mutation count was accepted") - } - short = append(short, "-session-mutation-count", "2") - if config, err := parseFlags(short, &bytes.Buffer{}); err != nil || config.sessionMutationCount != 2 { - t.Fatalf("typed short-session flags = %#v, %v", config, err) - } -} - -func TestMissingBrowserCapabilityFailsClosedAtPreflight(t *testing.T) { - directory := t.TempDir() - fixturePath := filepath.Join(directory, "fixture.bin") - wasmPath := filepath.Join(directory, "writer.wasm") - wasmExecPath := filepath.Join(directory, "wasm_exec.js") - for path, value := range map[string][]byte{fixturePath: []byte("fixture"), wasmPath: []byte("wasm"), wasmExecPath: []byte("exec")} { - if err := os.WriteFile(path, value, 0o600); err != nil { - t.Fatal(err) - } - } - config := hostConfig{ - gatewayBaseURL: "http://127.0.0.1:1", gatewayToken: strings.Repeat("a", 64), fixturePath: fixturePath, - workerID: "worker", platformID: "browser", clientKind: rq2wire.ClientBrowserWASM, backend: "kzg", - lifecycle: rq2wire.LifecycleBrowserCold, requestTimeout: time.Second, - steadyWarmup: "map-replace", - browserPath: "/definitely/missing/chromium", browserEngine: "chromium-150.0.7871.114", - browserPin: artifactPin{path: "/definitely/missing/chromium", sha256: strings.Repeat("b", 64), bytes: 1, executable: true, allowSymlink: true, maxBytes: maxBrowserLauncherBytes}, - wasmPin: testArtifactPin(t, wasmPath, maxWASMBytes), wasmExecPin: testArtifactPin(t, wasmExecPath, maxSupportBytes), - machinePin: testMachinePin(t, false), - } - worker, err := newBrowserWorker(config) - if err != nil { - t.Fatal(err) - } - request := preflightRequest(config) - record := worker.preflight(context.Background(), request) - if record.Success || record.FailureClass != "capability_unavailable" || !strings.Contains(record.Error, "browser launcher unavailable") { - t.Fatalf("missing browser preflight = %#v", record) - } - if err := record.Validate(); err != nil { - t.Fatalf("failed preflight is not protocol-valid: %v", err) - } -} - -func TestDecodeWorkerRequestRejectsUnknownAndTrailingData(t *testing.T) { - raw := `{"schema_version":"malt-rq2-worker-request/v1","worker_id":"worker","request_id":"preflight","record_kind":"preflight","session_id":"session","client_kind":"browser-wasm","platform_id":"browser","backend":"kzg","lifecycle":"browser-cold","fixture_id":"fixture","measured":false}` - if _, err := decodeWorkerRequest([]byte(raw)); err != nil { - t.Fatal(err) - } - for _, hostile := range []string{ - strings.TrimSuffix(raw, "}") + `,"extra":true}`, - strings.Replace(raw, `"worker_id":"worker"`, `"worker_id":"worker","worker_id":"other"`, 1), - raw + `{}`, - } { - if _, err := decodeWorkerRequest([]byte(hostile)); err == nil { - t.Fatalf("hostile JSON was accepted: %s", hostile) - } - } -} - -func TestRunMissingBrowserEmitsOneStrictFailedPreflightLine(t *testing.T) { - directory := t.TempDir() - fixturePath := filepath.Join(directory, "fixture.bin") - wasmPath := filepath.Join(directory, "writer.wasm") - wasmExecPath := filepath.Join(directory, "wasm_exec.js") - for path, value := range map[string][]byte{fixturePath: []byte("fixture"), wasmPath: []byte("wasm"), wasmExecPath: []byte("exec")} { - if err := os.WriteFile(path, value, 0o600); err != nil { - t.Fatal(err) - } - } - wasmPin := testArtifactPin(t, wasmPath, maxWASMBytes) - wasmExecPin := testArtifactPin(t, wasmExecPath, maxSupportBytes) - machinePin := testMachinePin(t, false) - token := strings.Repeat("a", 64) - args := []string{ - "-gateway-base-url", "http://127.0.0.1:1", "-gateway-instance-token", token, - "-fixture", fixturePath, "-worker-id", "worker", "-platform-id", "browser", - "-client-kind", "browser-wasm", "-backend", "kzg", "-lifecycle", "browser-cold", "-low-power-arm=false", - "-steady-warmup-operation", "map-replace", "-browser-path", "/definitely/missing/chromium", - "-browser-engine", "chromium-150.0.7871.114", "-browser-launcher-sha256", strings.Repeat("b", 64), "-browser-launcher-bytes", "1", - "-wasm", wasmPath, "-wasm-sha256", wasmPin.sha256, "-wasm-bytes", strconv.FormatInt(wasmPin.bytes, 10), - "-wasm-exec", wasmExecPath, "-wasm-exec-sha256", wasmExecPin.sha256, "-wasm-exec-bytes", strconv.FormatInt(wasmExecPin.bytes, 10), - "-machine-descriptor", machinePin.path, "-machine-descriptor-sha256", machinePin.sha256, "-machine-descriptor-bytes", strconv.FormatInt(machinePin.bytes, 10), - } - config, err := parseFlags(args, io.Discard) - if err != nil { - t.Fatal(err) - } - requestJSON, _ := json.Marshal(preflightRequest(config)) - var stdout bytes.Buffer - err = run(args, bytes.NewReader(append(requestJSON, '\n')), &stdout, io.Discard) - if err == nil || !strings.Contains(err.Error(), "closed before session-end") { - t.Fatalf("run error = %v", err) - } - if bytes.Count(stdout.Bytes(), []byte{'\n'}) != 1 { - t.Fatalf("stdout is not one strict JSONL record: %q", stdout.String()) - } - var record rq2wire.WorkerRecord - if err := strictJSON(bytes.TrimSpace(stdout.Bytes()), &record); err != nil { - t.Fatal(err) - } - if record.Success || record.FailureClass != "capability_unavailable" { - t.Fatalf("missing-browser stdout = %#v", record) - } -} - -func TestRealChromiumExecutesColdAndSteadyBrowserOperations(t *testing.T) { - if testing.Short() || runtime.GOOS != "linux" { - t.Skip("real Chromium/WASM integration requires a non-short Linux test") - } - browserPath, err := exec.LookPath("chromium") - if err != nil { - browserPath, err = exec.LookPath("google-chrome") - } - if err != nil { - t.Skip("Chromium-family browser is not installed") - } - versionCommand := exec.Command(browserPath, "--version") - versionOutput, err := versionCommand.CombinedOutput() - if err != nil { - t.Skipf("browser version probe unavailable: %v", err) - } - engine, err := parseBrowserEngineOutput(string(versionOutput)) - if err != nil { - t.Skipf("unsupported installed browser: %v", err) - } - directory := t.TempDir() - wasmPath := filepath.Join(directory, "writer.wasm") - moduleRoot := filepath.Clean(filepath.Join("..", "..", "..", "..")) - build := exec.Command("go", "build", "-p=6", "-buildvcs=false", "-o", wasmPath, "./tools/evaluation/cmd/malt-eval-rq2-browser-wasm") - build.Dir = moduleRoot - build.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", "CGO_ENABLED=0") - if output, err := build.CombinedOutput(); err != nil { - t.Fatalf("build real Go/WASM writer: %v\n%s", err, output) - } - wasmExecPath := filepath.Join(runtime.GOROOT(), "lib", "wasm", "wasm_exec.js") - if _, err := os.Stat(wasmExecPath); err != nil { - t.Skipf("matching wasm_exec.js is unavailable: %v", err) - } - browserPin := testArtifactPinAllowSymlink(t, browserPath, maxBrowserLauncherBytes, true) - wasmPin := testArtifactPin(t, wasmPath, maxWASMBytes) - wasmExecPin := testArtifactPin(t, wasmExecPath, maxSupportBytes) - testCases := []struct { - name string - backend string - lifecycle string - sessionMutationCount int - operations []testBrowserOperation - }{ - {name: "kzg-cold", backend: "kzg", lifecycle: rq2wire.LifecycleBrowserCold, operations: []testBrowserOperation{{name: "document-edit-cid-binding-submit", measured: true}}}, - {name: "kzg-steady", backend: "kzg", lifecycle: rq2wire.LifecycleBrowserSteady, operations: []testBrowserOperation{ - {name: "map-replace", measured: false}, - {name: "document-edit-cid-binding-submit", measured: true}, {name: "list-append", measured: true}, - {name: "list-replace", measured: true}, {name: "map-insert", measured: true}, {name: "map-replace", measured: true}, - }}, - {name: "ipa-cold", backend: "ipa", lifecycle: rq2wire.LifecycleBrowserCold, operations: []testBrowserOperation{{name: "map-replace", measured: true}}}, - {name: "kzg-short-n2", backend: "kzg", lifecycle: rq2wire.LifecycleBrowserShort, sessionMutationCount: 2, operations: []testBrowserOperation{ - {name: "document-edit-cid-binding-submit", measured: true}, {name: "document-edit-cid-binding-submit", measured: true}, - }}, - {name: "kzg-short-baseline-n2", backend: "kzg", lifecycle: rq2wire.LifecycleBrowserSteady, sessionMutationCount: 2, operations: []testBrowserOperation{ - {name: "map-replace", measured: false}, - {name: "document-edit-cid-binding-submit", measured: true}, {name: "document-edit-cid-binding-submit", measured: true}, - }}, - } - for caseIndex, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - token := strings.Repeat(string(rune('a'+caseIndex)), 64) - gatewayFixture, initialRoot, fixtureBytes := newTestBrowserGateway(t, token, testCase.backend) - fixturePath := filepath.Join(t.TempDir(), "fixture.bin") - if err := os.WriteFile(fixturePath, fixtureBytes, 0o600); err != nil { - t.Fatal(err) - } - gateway := gatewayFixture - defer gateway.Close() - config := hostConfig{ - gatewayBaseURL: gateway.URL(), gatewayToken: token, fixturePath: fixturePath, - workerID: "worker-" + testCase.name, platformID: "browser", clientKind: rq2wire.ClientBrowserWASM, backend: testCase.backend, - lifecycle: testCase.lifecycle, sessionMutationCount: testCase.sessionMutationCount, requestTimeout: 2 * time.Minute, - steadyWarmup: "map-replace", - browserPath: browserPath, browserEngine: engine, browserPin: browserPin, - wasmPin: wasmPin, wasmExecPin: wasmExecPin, - machinePin: testMachinePin(t, false), - } - worker, err := newBrowserWorker(config) - if err != nil { - t.Fatal(err) - } - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancel() - exchange := func(request rq2wire.WorkerRequest) rq2wire.WorkerRecord { - t.Helper() - if err := worker.bindRequest(request); err != nil { - t.Fatal(err) - } - record := worker.exchange(ctx, request) - if err := rq2wire.Bind(record, request); err != nil { - t.Fatal(err) - } - if err := record.Validate(); err != nil { - t.Fatalf("invalid real-browser record: %v\n%#v", err, record) - } - if !record.Success { - t.Fatalf("real browser request %s failed: %s: %s", request.RequestID, record.FailureClass, record.Error) - } - return record - } - preflight := exchange(preflightRequest(config)) - if preflight.Runtime == nil || preflight.Runtime.BrowserEngine != engine || preflight.Runtime.WASMSHA256 != wasmPin.sha256 { - t.Fatalf("real browser runtime provenance = %#v", preflight.Runtime) - } - start := requestFor(config, rq2wire.RecordSessionStart, "session-start", "", false, initialRoot.String()) - exchange(start) - acceptedRoot := initialRoot.String() - for index, operation := range testCase.operations { - request := requestFor(config, rq2wire.RecordMutation, fmt.Sprintf("mutation-%03d", index), operation.name, operation.measured, acceptedRoot) - record := exchange(request) - acceptedRoot = record.Mutation.ReceiptRoot - metrics := record.Mutation.Metrics - if !metrics.CPUTotal.Applicable || !metrics.PeakMemory.Applicable || metrics.PeakMemory.Bytes == 0 || - !metrics.JSWASMBoundary.Applicable || metrics.JSWASMBoundary.Bytes == 0 { - t.Fatalf("real browser resource/boundary metrics are incomplete: %#v", metrics) - } - cold := testCase.lifecycle == rq2wire.LifecycleBrowserCold || testCase.lifecycle == rq2wire.LifecycleBrowserShort && index == 0 - if metrics.WASMDownload.Applicable != cold || metrics.WASMInstantiate.Applicable != cold || metrics.ParameterLoad.Applicable != cold || metrics.FirstMutation.Applicable != cold { - t.Fatalf("cold/steady phase classification is wrong: %#v", metrics) - } - } - exchange(requestFor(config, rq2wire.RecordSessionEnd, "session-end", "", false, acceptedRoot)) - if err := worker.browser.close(); err != nil { - t.Fatalf("close real browser: %v", err) - } - }) - } -} - -type testBrowserOperation struct { - name string - measured bool -} - -func requestFor(config hostConfig, kind, requestID, operation string, measured bool, root string) rq2wire.WorkerRequest { - return rq2wire.WorkerRequest{ - SchemaVersion: rq2wire.WorkerRequestSchema, WorkerID: config.workerID, RequestID: requestID, - RecordKind: kind, SessionID: "session", ClientKind: rq2wire.ClientBrowserWASM, PlatformID: config.platformID, - Backend: config.backend, Lifecycle: config.lifecycle, FixtureID: "fixture", Operation: operation, - Measured: measured, ExpectedAcceptedRoot: root, - } -} - -func newTestBrowserGateway(t *testing.T, token, backend string) (*rq2e0.ConformanceGateway, cid.Cid, []byte) { - t.Helper() - fixture := testBrowserSourceFixture(t) - gateway, root, err := rq2e0.NewConformanceGateway(fixture, backend, token) - if err != nil { - t.Fatal(err) - } - raw, err := json.Marshal(fixture) - if err != nil { - gateway.Close() - t.Fatal(err) - } - return gateway, root, raw -} - -func testBrowserSourceFixture(t *testing.T) *rq2fixture.Fixture { - t.Helper() - document := []byte("document") - chunkA := make([]byte, 32) - chunkB := make([]byte, 32) - copy(chunkA, "chunk-a") - copy(chunkB, "chunk-b") - index := uint64(0) - seed := sha256.Sum256([]byte("browser source fixture seed")) - value := rq2fixture.SourceDefinition{ - SchemaVersion: rq2fixture.SourceSchemaVersion, FixtureID: "fixture", MutationSeedSHA256: hex.EncodeToString(seed[:]), - DirectFiles: []rq2fixture.SourceDirectFile{{Path: "document.txt", Coordinate: "document.txt", Bytes: document}}, - ListFiles: []rq2fixture.SourceListFile{{ - Path: "list.bin", Coordinate: "list.bin", ChunkSize: 32, TotalSize: 64, - Chunks: []rq2fixture.SourceListChunk{{Index: 0, Bytes: chunkA}, {Index: 1, Bytes: chunkB}}, - }}, - Operations: []rq2fixture.Operation{ - {Name: "document-edit-cid-binding-submit", Kind: rq2fixture.KindDocumentEdit, SourcePath: "document.txt", SourceCoordinate: "document.txt", PayloadBytes: 32}, - {Name: "map-replace", Kind: rq2fixture.KindDirectReplace, SourcePath: "document.txt", SourceCoordinate: "document.txt", PayloadBytes: 16}, - {Name: "map-insert", Kind: rq2fixture.KindDirectInsert, DestinationPath: "inserted.txt", DestinationCoordinate: "inserted.txt", PayloadBytes: 16}, - {Name: "list-append", Kind: rq2fixture.KindListAppend, SourcePath: "list.bin", SourceCoordinate: "list.bin", PayloadBytes: 32}, - {Name: "list-replace", Kind: rq2fixture.KindListReplace, SourcePath: "list.bin", SourceCoordinate: "list.bin", PayloadBytes: 32, ListIndex: &index}, - }, - } - fixture, err := rq2e0.BuildFixture(t.Context(), &value) - if err != nil { - t.Fatal(err) - } - return fixture -} - -func preflightRequest(config hostConfig) rq2wire.WorkerRequest { - return rq2wire.WorkerRequest{ - SchemaVersion: rq2wire.WorkerRequestSchema, WorkerID: config.workerID, RequestID: "preflight", - RecordKind: rq2wire.RecordPreflight, SessionID: "session", ClientKind: rq2wire.ClientBrowserWASM, - PlatformID: config.platformID, Backend: config.backend, Lifecycle: config.lifecycle, FixtureID: "fixture", - } -} - -func testArtifactPin(t *testing.T, path string, maximum int64) artifactPin { - t.Helper() - return testArtifactPinAllowSymlink(t, path, maximum, false) -} - -func testMachinePin(t *testing.T, lowPower bool) artifactPin { - t.Helper() - identity, err := machine.Probe() - if err != nil { - t.Fatal(err) - } - classification := machine.ClassGeneral - if lowPower { - classification = machine.ClassLowPower - } - descriptor, err := machine.NewDescriptor("test-machine", classification, "test-suite:registered-platform-evidence", identity) - if err != nil { - t.Fatal(err) - } - raw, err := json.Marshal(descriptor) - if err != nil { - t.Fatal(err) - } - path := filepath.Join(t.TempDir(), "machine.json") - if err := os.WriteFile(path, raw, 0o600); err != nil { - t.Fatal(err) - } - digest := sha256.Sum256(raw) - return artifactPin{path: path, sha256: hex.EncodeToString(digest[:]), bytes: int64(len(raw)), maxBytes: machine.MaxDescriptorBytes} -} - -func testArtifactPinAllowSymlink(t *testing.T, path string, maximum int64, executable bool) artifactPin { - t.Helper() - resolved, err := filepath.EvalSymlinks(path) - if err != nil { - t.Fatal(err) - } - data, err := os.ReadFile(resolved) - if err != nil { - t.Fatal(err) - } - digest := sha256.Sum256(data) - return artifactPin{ - path: path, sha256: hex.EncodeToString(digest[:]), bytes: int64(len(data)), executable: executable, - allowSymlink: executable, maxBytes: maximum, - } -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/process_linux.go b/tools/evaluation/cmd/malt-eval-rq2-browser-worker/process_linux.go deleted file mode 100644 index 3cba4a5..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/process_linux.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build linux - -package main - -import ( - "errors" - "os/exec" - "syscall" -) - -func configureBrowserProcess(command *exec.Cmd) { - command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} -} - -func forceKillBrowserProcess(command *exec.Cmd) error { - if command == nil || command.Process == nil { - return nil - } - err := syscall.Kill(-command.Process.Pid, syscall.SIGKILL) - if errors.Is(err, syscall.ESRCH) { - return nil - } - return err -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/process_other.go b/tools/evaluation/cmd/malt-eval-rq2-browser-worker/process_other.go deleted file mode 100644 index b474c21..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/process_other.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build !linux - -package main - -import "os/exec" - -func configureBrowserProcess(_ *exec.Cmd) {} - -func forceKillBrowserProcess(command *exec.Cmd) error { - if command == nil || command.Process == nil { - return nil - } - return command.Process.Kill() -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/resident_linux.go b/tools/evaluation/cmd/malt-eval-rq2-browser-worker/resident_linux.go deleted file mode 100644 index 3cdb2c9..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/resident_linux.go +++ /dev/null @@ -1,111 +0,0 @@ -//go:build linux - -package main - -import ( - "errors" - "fmt" - "os" - "strconv" - "strings" - "time" -) - -type residentSampler struct { - stopChannel chan struct{} - done chan struct{} - peakBytes uint64 - samples uint64 - err error -} - -func startResidentSampler(processID int) (*residentSampler, error) { - initial, err := processTreeResidentBytes(processID) - if err != nil { - return nil, fmt.Errorf("sample Chromium process tree RSS: %w", err) - } - if initial == 0 { - return nil, fmt.Errorf("sample Chromium process tree RSS: zero bytes") - } - sampler := &residentSampler{stopChannel: make(chan struct{}), done: make(chan struct{}), peakBytes: initial, samples: 1} - go func() { - defer close(sampler.done) - ticker := time.NewTicker(2 * time.Millisecond) - defer ticker.Stop() - for { - select { - case <-sampler.stopChannel: - value, err := processTreeResidentBytes(processID) - if err == nil { - sampler.samples++ - sampler.peakBytes = max(sampler.peakBytes, value) - } else if !errors.Is(err, os.ErrNotExist) { - sampler.err = err - } - return - case <-ticker.C: - value, err := processTreeResidentBytes(processID) - if err != nil { - if !errors.Is(err, os.ErrNotExist) { - sampler.err = err - } - continue - } - sampler.samples++ - sampler.peakBytes = max(sampler.peakBytes, value) - } - } - }() - return sampler, nil -} - -func (s *residentSampler) stop() (uint64, uint64, error) { - close(s.stopChannel) - <-s.done - return s.peakBytes, s.samples, s.err -} - -func processTreeResidentBytes(root int) (uint64, error) { - queue := []int{root} - seen := map[int]struct{}{} - var total uint64 - for len(queue) != 0 { - processID := queue[0] - queue = queue[1:] - if _, exists := seen[processID]; exists { - continue - } - seen[processID] = struct{}{} - statm, err := os.ReadFile(fmt.Sprintf("/proc/%d/statm", processID)) - if err != nil { - if processID == root { - return 0, err - } - continue - } - fields := strings.Fields(string(statm)) - if len(fields) < 2 { - return 0, fmt.Errorf("process %d statm is malformed", processID) - } - residentPages, err := strconv.ParseUint(fields[1], 10, 64) - if err != nil { - return 0, err - } - total += residentPages * uint64(os.Getpagesize()) - children, err := os.ReadFile(fmt.Sprintf("/proc/%d/task/%d/children", processID, processID)) - if err != nil { - if !errors.Is(err, os.ErrNotExist) { - return 0, err - } - continue - } - for _, raw := range strings.Fields(string(children)) { - child, err := strconv.Atoi(raw) - if err != nil || child <= 0 { - return 0, fmt.Errorf("process %d has invalid child PID %q", processID, raw) - } - queue = append(queue, child) - } - } - return total, nil -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/resident_other.go b/tools/evaluation/cmd/malt-eval-rq2-browser-worker/resident_other.go deleted file mode 100644 index 59ee383..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/resident_other.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build !linux - -package main - -import "fmt" - -type residentSampler struct{} - -func startResidentSampler(_ int) (*residentSampler, error) { - return nil, fmt.Errorf("real browser peak-RSS sampling requires Linux /proc") -} - -func (s *residentSampler) stop() (uint64, uint64, error) { - return 0, 0, fmt.Errorf("real browser peak-RSS sampling requires Linux /proc") -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/selftest.go b/tools/evaluation/cmd/malt-eval-rq2-browser-worker/selftest.go deleted file mode 100644 index 2fbba76..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/selftest.go +++ /dev/null @@ -1,888 +0,0 @@ -package main - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "net/http" - "net/http/httptest" - "net/http/httputil" - "net/url" - "os" - "os/exec" - "path/filepath" - "slices" - "strconv" - "strings" - "sync" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/e0selftest" - "github.com/dewebprotocol/malt-client/internal/evaluation/machine" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2e0" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2wire" - cid "github.com/ipfs/go-cid" -) - -const ( - browserE0CapabilityID = "rq2.wasm-writer" - browserE0ProfileID = "rq2-wasm-writer-positive-hostile-v2" - // Updated only when the ordered compiled case contract intentionally - // changes together with the evaluator-owned registry. - browserE0ExpectedProfileSHA256 = "53449df9e3f9dd285e0b2db48044c03b4bbbce9797f56e5c0a902ae9d830deb0" -) - -var browserE0Profile = e0selftest.Profile{ - ProfileID: browserE0ProfileID, - PositiveCases: []string{ - "execute-browser-kzg-cold-document-flow", - "execute-browser-kzg-steady-operation-matrix", - "execute-browser-ipa-cold-map-replace", - "execute-browser-kzg-short-session-continuity", - }, - HostileCases: []string{ - "reject-malformed-request", - "reject-wasm-pin-substitution", - "reject-steady-without-warmup", - "reject-backend-root-substitution", - "reject-browser-engine-pin-substitution", - "reject-hung-gateway-timeout-and-late-commit", - }, -} - -var browserE0RequiredOperations = []string{ - "document-edit-cid-binding-submit", "list-append", "list-replace", "map-insert", "map-replace", -} - -type browserE0Config struct { - fixturePath string - machinePath string - browserPath string - wasmPath string - wasmExecPath string - timeout time.Duration - - fixture *rq2fixture.Fixture - machine *machine.Evidence - browserEngine string - browserPin artifactPin - wasmPin artifactPin - wasmExecPin artifactPin - machinePin artifactPin -} - -type browserE0Operation struct { - name string - measured bool -} - -type browserE0Case struct { - backend string - lifecycle string - sessionMutationCount int - operations []browserE0Operation -} - -func runBrowserE0SelfTest(arguments []string, output, stderr io.Writer) error { - config, err := parseBrowserE0Flags(arguments, stderr) - if err != nil { - return err - } - config, err = prepareBrowserE0(config) - if err != nil { - return err - } - contract, err := browserE0Profile.Contract() - if err != nil { - return err - } - if contract.ProfileSHA256 != browserE0ExpectedProfileSHA256 { - return fmt.Errorf("compiled wasm-writer E0 profile digest drifted: got %s", contract.ProfileSHA256) - } - results := make([]e0selftest.CaseResult, 0, len(browserE0Profile.PositiveCases)+len(browserE0Profile.HostileCases)) - for _, caseID := range browserE0Profile.PositiveCases { - if err := executeBrowserE0Positive(config, caseID); err != nil { - return fmt.Errorf("browser E0 positive case %q: %w", caseID, err) - } - results = append(results, e0selftest.CaseResult{ID: caseID, Passed: true}) - } - for _, caseID := range browserE0Profile.HostileCases { - if err := executeBrowserE0Hostile(config, caseID); err != nil { - return fmt.Errorf("browser E0 hostile case %q: %w", caseID, err) - } - results = append(results, e0selftest.CaseResult{ID: caseID, Passed: true}) - } - receipt, err := e0selftest.Issue(browserE0CapabilityID, browserE0Profile, results) - if err != nil { - return err - } - encoder := json.NewEncoder(output) - encoder.SetEscapeHTML(false) - return encoder.Encode(receipt) -} - -func parseBrowserE0Flags(arguments []string, stderr io.Writer) (browserE0Config, error) { - flags := flag.NewFlagSet("malt-eval-rq2-browser-worker --e0-self-test", flag.ContinueOnError) - flags.SetOutput(stderr) - fixture := flags.String("fixture", "", "pinned RQ2 source fixture corpus") - machinePath := flags.String("machine-descriptor", "", "pinned platform machine descriptor") - browserPath := flags.String("browser-launcher", "", "pinned Chromium-family executable") - wasmPath := flags.String("wasm", "", "pinned Go/WASM writer") - wasmExecPath := flags.String("wasm-exec", "", "pinned matching wasm_exec.js") - timeout := flags.Duration("request-timeout", 3*time.Minute, "bounded per-request timeout") - if err := flags.Parse(arguments); err != nil { - return browserE0Config{}, err - } - if flags.NArg() != 0 || *fixture == "" || *machinePath == "" || *browserPath == "" || *wasmPath == "" || *wasmExecPath == "" || *timeout <= 0 || *timeout > 10*time.Minute { - return browserE0Config{}, errors.New("browser formal E0 requires fixture, machine descriptor, browser launcher, WASM, wasm_exec.js, and a bounded timeout") - } - config := browserE0Config{ - fixturePath: filepath.Clean(*fixture), machinePath: filepath.Clean(*machinePath), browserPath: filepath.Clean(*browserPath), - wasmPath: filepath.Clean(*wasmPath), wasmExecPath: filepath.Clean(*wasmExecPath), timeout: *timeout, - } - for _, path := range config.inputPaths() { - if !filepath.IsAbs(path) { - return browserE0Config{}, errors.New("browser formal E0 file paths must be absolute") - } - } - return config, nil -} - -func (c browserE0Config) inputPaths() []string { - return []string{c.fixturePath, c.machinePath, c.browserPath, c.wasmPath, c.wasmExecPath} -} - -func prepareBrowserE0(config browserE0Config) (browserE0Config, error) { - if err := validateBrowserE0InvocationPaths(config.inputPaths()); err != nil { - return browserE0Config{}, err - } - fixtureRaw, err := readFixture(config.fixturePath) - if err != nil { - return browserE0Config{}, err - } - config.fixture, err = rq2fixture.Decode(fixtureRaw) - if err != nil { - return browserE0Config{}, err - } - if err := config.fixture.RequireOperations(browserE0RequiredOperations); err != nil { - return browserE0Config{}, err - } - for _, backend := range []string{"kzg", "ipa"} { - if _, err := config.fixture.Root(backend); err != nil { - return browserE0Config{}, fmt.Errorf("browser formal E0 fixture %s root: %w", backend, err) - } - } - config.machinePin, err = describeBrowserE0Artifact(config.machinePath, machine.MaxDescriptorBytes, false) - if err != nil { - return browserE0Config{}, fmt.Errorf("machine descriptor: %w", err) - } - config.machine, err = machine.LoadPinned(config.machinePath, config.machinePin.sha256, config.machinePin.bytes) - if err != nil { - return browserE0Config{}, fmt.Errorf("machine descriptor: %w", err) - } - config.browserPin, err = describeBrowserE0Artifact(config.browserPath, maxBrowserLauncherBytes, true) - if err != nil { - return browserE0Config{}, fmt.Errorf("browser launcher: %w", err) - } - config.wasmPin, err = describeBrowserE0Artifact(config.wasmPath, maxWASMBytes, false) - if err != nil { - return browserE0Config{}, fmt.Errorf("Go/WASM writer: %w", err) - } - config.wasmExecPin, err = describeBrowserE0Artifact(config.wasmExecPath, maxSupportBytes, false) - if err != nil { - return browserE0Config{}, fmt.Errorf("wasm_exec.js: %w", err) - } - probeContext, cancel := context.WithTimeout(context.Background(), min(config.timeout, 30*time.Second)) - defer cancel() - config.browserEngine, err = probeBrowserE0Engine(probeContext, config.browserPath) - if err != nil { - return browserE0Config{}, err - } - return config, nil -} - -func validateBrowserE0InvocationPaths(consumed []string) error { - want, err := e0selftest.InputPaths() - if err != nil { - return err - } - consumed = slices.Clone(consumed) - slices.Sort(consumed) - if !slices.Equal(consumed, want) { - return fmt.Errorf("browser typed self-test flags consume %v, E0 invocation pins %v", consumed, want) - } - return nil -} - -func describeBrowserE0Artifact(path string, maximum int64, executable bool) (artifactPin, error) { - if !filepath.IsAbs(path) || filepath.Clean(path) != path || maximum <= 0 { - return artifactPin{}, errors.New("artifact path must be canonical and absolute") - } - info, err := os.Lstat(path) - if err != nil { - return artifactPin{}, err - } - if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() || info.Size() <= 0 || info.Size() > maximum { - return artifactPin{}, errors.New("artifact must be a bounded regular non-symlink file") - } - resolved, err := filepath.EvalSymlinks(path) - if err != nil || resolved != path { - return artifactPin{}, errors.New("artifact path or one of its parents must not be symlinked") - } - file, err := os.Open(path) - if err != nil { - return artifactPin{}, err - } - hash := sha256.New() - written, readErr := io.Copy(hash, io.LimitReader(file, maximum+1)) - closeErr := file.Close() - if readErr != nil { - return artifactPin{}, readErr - } - if closeErr != nil { - return artifactPin{}, closeErr - } - post, err := os.Lstat(path) - if err != nil || !os.SameFile(info, post) || post.Mode()&os.ModeSymlink != 0 || written != info.Size() { - return artifactPin{}, errors.New("artifact changed while it was hashed") - } - pin := artifactPin{path: path, sha256: hex.EncodeToString(hash.Sum(nil)), bytes: written, executable: executable, maxBytes: maximum} - if _, _, err := loadPinnedArtifact(pin, false); err != nil { - return artifactPin{}, err - } - return pin, nil -} - -func probeBrowserE0Engine(ctx context.Context, browserPath string) (string, error) { - output, err := exec.CommandContext(ctx, browserPath, "--version").CombinedOutput() - if err != nil { - return "", fmt.Errorf("run pinned browser version probe: %w: %s", err, boundedText(output, 1024)) - } - engine, err := parseBrowserEngineOutput(string(output)) - if err != nil { - return "", err - } - return engine, nil -} - -func executeBrowserE0Positive(config browserE0Config, caseID string) error { - testCase, err := browserE0PositiveCase(caseID) - if err != nil { - return err - } - harness, gateway, root, err := startBrowserE0Worker(config, testCase, caseID, browserE0Overrides{}) - if err != nil { - return err - } - defer gateway.Close() - if err := runBrowserE0Session(harness, config, testCase, caseID, root); err != nil { - harness.Abort() - return err - } - if err := harness.Close(); err != nil { - return err - } - if got, want := gateway.Operations(), uint64(len(testCase.operations)); got != want { - return fmt.Errorf("conformance Gateway accepted %d operations, want %d", got, want) - } - return nil -} - -func browserE0PositiveCase(caseID string) (browserE0Case, error) { - switch caseID { - case "execute-browser-kzg-cold-document-flow": - return browserE0Case{backend: "kzg", lifecycle: rq2wire.LifecycleBrowserCold, operations: []browserE0Operation{{name: "document-edit-cid-binding-submit", measured: true}}}, nil - case "execute-browser-kzg-steady-operation-matrix": - return browserE0Case{backend: "kzg", lifecycle: rq2wire.LifecycleBrowserSteady, operations: []browserE0Operation{ - {name: "map-replace", measured: false}, - {name: "document-edit-cid-binding-submit", measured: true}, - {name: "list-append", measured: true}, - {name: "list-replace", measured: true}, - {name: "map-insert", measured: true}, - {name: "map-replace", measured: true}, - }}, nil - case "execute-browser-ipa-cold-map-replace": - return browserE0Case{backend: "ipa", lifecycle: rq2wire.LifecycleBrowserCold, operations: []browserE0Operation{{name: "map-replace", measured: true}}}, nil - case "execute-browser-kzg-short-session-continuity": - return browserE0Case{backend: "kzg", lifecycle: rq2wire.LifecycleBrowserShort, sessionMutationCount: 2, operations: []browserE0Operation{ - {name: "document-edit-cid-binding-submit", measured: true}, - {name: "document-edit-cid-binding-submit", measured: true}, - }}, nil - default: - return browserE0Case{}, errors.New("unknown browser E0 positive case") - } -} - -type browserE0Overrides struct { - browserSHA string - wasmSHA string - engine string -} - -func startBrowserE0Worker(config browserE0Config, testCase browserE0Case, caseID string, overrides browserE0Overrides) (*rq2e0.Harness, *rq2e0.ConformanceGateway, cid.Cid, error) { - tokenDigest := sha256.Sum256([]byte("malt-rq2-browser-e0\x00" + caseID)) - token := hex.EncodeToString(tokenDigest[:]) - gateway, root, err := rq2e0.NewConformanceGateway(config.fixture, testCase.backend, token) - if err != nil { - return nil, nil, cid.Undef, err - } - args := browserE0WorkerArguments(config, testCase, caseID, gateway.URL(), token, overrides) - harness, err := rq2e0.StartHarness(config.timeout, func(input io.Reader, output io.Writer) error { - return run(args, input, output, io.Discard) - }) - if err != nil { - gateway.Close() - return nil, nil, cid.Undef, err - } - return harness, gateway, root, nil -} - -func browserE0WorkerArguments(config browserE0Config, testCase browserE0Case, caseID, gatewayURL, token string, overrides browserE0Overrides) []string { - browserSHA := config.browserPin.sha256 - if overrides.browserSHA != "" { - browserSHA = overrides.browserSHA - } - wasmSHA := config.wasmPin.sha256 - if overrides.wasmSHA != "" { - wasmSHA = overrides.wasmSHA - } - engine := config.browserEngine - if overrides.engine != "" { - engine = overrides.engine - } - return []string{ - "-gateway-base-url", gatewayURL, "-gateway-instance-token", token, - "-fixture", config.fixturePath, "-worker-id", "e0-browser-" + shortBrowserE0ID(caseID), "-platform-id", "e0-browser", - "-client-kind", rq2wire.ClientBrowserWASM, "-backend", testCase.backend, "-lifecycle", testCase.lifecycle, - "-steady-warmup-operation", "map-replace", "-session-mutation-count", strconv.Itoa(testCase.sessionMutationCount), - "-low-power-arm=" + strconv.FormatBool(config.machine.Descriptor.LowPowerARM()), "-request-timeout", config.timeout.String(), - "-browser-path", config.browserPath, "-browser-engine", engine, - "-browser-launcher-sha256", browserSHA, "-browser-launcher-bytes", strconv.FormatInt(config.browserPin.bytes, 10), - "-wasm", config.wasmPath, "-wasm-sha256", wasmSHA, "-wasm-bytes", strconv.FormatInt(config.wasmPin.bytes, 10), - "-wasm-exec", config.wasmExecPath, "-wasm-exec-sha256", config.wasmExecPin.sha256, "-wasm-exec-bytes", strconv.FormatInt(config.wasmExecPin.bytes, 10), - "-machine-descriptor", config.machinePath, "-machine-descriptor-sha256", config.machinePin.sha256, "-machine-descriptor-bytes", strconv.FormatInt(config.machinePin.bytes, 10), - } -} - -func runBrowserE0Session(harness *rq2e0.Harness, config browserE0Config, testCase browserE0Case, caseID string, root cid.Cid) error { - preflight := browserE0Request(config.fixture.FixtureID, testCase, caseID, "preflight", rq2wire.RecordPreflight, "", false, "") - preflightRecord, err := browserE0Exchange(harness, preflight, true) - if err != nil { - return err - } - if preflightRecord.Runtime == nil || preflightRecord.Runtime.BrowserEngine != config.browserEngine || preflightRecord.Runtime.WASMSHA256 != config.wasmPin.sha256 { - return fmt.Errorf("browser preflight did not bind pinned engine/WASM provenance: %+v", preflightRecord.Runtime) - } - start := browserE0Request(config.fixture.FixtureID, testCase, caseID, "session-start", rq2wire.RecordSessionStart, "", false, root.String()) - if _, err := browserE0Exchange(harness, start, true); err != nil { - return err - } - accepted := root.String() - for index, operation := range testCase.operations { - request := browserE0Request(config.fixture.FixtureID, testCase, caseID, fmt.Sprintf("mutation-%02d", index), rq2wire.RecordMutation, operation.name, operation.measured, accepted) - record, err := browserE0Exchange(harness, request, true) - if err != nil { - return err - } - if record.Mutation == nil || record.Mutation.PriorRoot != accepted || !record.Mutation.ReceiptAccepted || record.Mutation.ReceiptRoot != record.Mutation.CandidateRoot { - return fmt.Errorf("browser mutation continuity/receipt evidence is incomplete: %+v", record.Mutation) - } - if err := validateBrowserE0LifecycleMetrics(config, testCase, index, record.Mutation.Metrics); err != nil { - return fmt.Errorf("browser mutation %d lifecycle metrics: %w", index, err) - } - accepted = record.Mutation.ReceiptRoot - } - end := browserE0Request(config.fixture.FixtureID, testCase, caseID, "session-end", rq2wire.RecordSessionEnd, "", false, accepted) - endRecord, err := browserE0Exchange(harness, end, true) - if err != nil { - return err - } - if endRecord.Session == nil || endRecord.Session.AcceptedRoot != accepted || !endRecord.Session.AuditPassed || endRecord.Session.ReceiptCount != uint64(len(testCase.operations)) { - return fmt.Errorf("browser session-end audit is incomplete: %+v", endRecord.Session) - } - return nil -} - -func validateBrowserE0LifecycleMetrics(config browserE0Config, testCase browserE0Case, index int, metrics rq2wire.MutationMetrics) error { - if !metrics.MutationTotal.Applicable || metrics.MutationTotal.DurationNS == 0 { - return errors.New("inclusive browser mutation total is absent") - } - if !metrics.JSWASMBoundary.Applicable || metrics.JSWASMBoundary.Count == 0 || metrics.JSWASMBoundary.Bytes == 0 { - return errors.New("real JS/WASM boundary evidence is absent") - } - cold := testCase.lifecycle == rq2wire.LifecycleBrowserCold || testCase.lifecycle == rq2wire.LifecycleBrowserShort && index == 0 - phases := []rq2wire.PhaseMeasurement{metrics.WASMDownload, metrics.WASMInstantiate, metrics.ParameterLoad, metrics.FirstMutation} - for _, phase := range phases { - if phase.Applicable != cold { - return fmt.Errorf("cold phase applicability=%t, want %t", phase.Applicable, cold) - } - if cold && (phase.Status != rq2wire.PhaseObserved || phase.Count == 0) { - return errors.New("cold phase lacks a real observation") - } - if !cold && (phase.Status != rq2wire.PhaseNotApplicable || phase.DurationNS != 0 || phase.Bytes != 0 || phase.Count != 0) { - return errors.New("steady/later short-session mutation contains cold-start evidence") - } - } - if !cold { - return nil - } - _, _, parameterBytes, ok := rq2wire.ParameterEvidence(testCase.backend) - if !ok || metrics.WASMDownload.Bytes != uint64(config.wasmPin.bytes) || metrics.ParameterLoad.Bytes != parameterBytes || metrics.WASMInstantiate.DurationNS == 0 || metrics.FirstMutation.DurationNS == 0 { - return fmt.Errorf("cold evidence does not bind exact WASM/parameter bytes and nonzero instantiate/first-mutation time: %+v", metrics) - } - return nil -} - -func browserE0Exchange(harness *rq2e0.Harness, request rq2wire.WorkerRequest, requireSuccess bool) (rq2wire.WorkerRecord, error) { - var record rq2wire.WorkerRecord - if err := harness.Exchange(request, &record); err != nil { - return rq2wire.WorkerRecord{}, err - } - if err := rq2wire.Bind(record, request); err != nil { - return rq2wire.WorkerRecord{}, err - } - if err := record.Validate(); err != nil { - return rq2wire.WorkerRecord{}, err - } - if requireSuccess && !record.Success { - return rq2wire.WorkerRecord{}, fmt.Errorf("browser worker failed: %s: %s", record.FailureClass, record.Error) - } - return record, nil -} - -func browserE0Request(fixtureID string, testCase browserE0Case, caseID, requestID, kind, operation string, measured bool, root string) rq2wire.WorkerRequest { - return rq2wire.WorkerRequest{ - SchemaVersion: rq2wire.WorkerRequestSchema, WorkerID: "e0-browser-" + shortBrowserE0ID(caseID), RequestID: requestID, - RecordKind: kind, SessionID: "session-" + shortBrowserE0ID(caseID), ClientKind: rq2wire.ClientBrowserWASM, - PlatformID: "e0-browser", Backend: testCase.backend, Lifecycle: testCase.lifecycle, FixtureID: fixtureID, - Operation: operation, Measured: measured, ExpectedAcceptedRoot: root, - } -} - -func executeBrowserE0Hostile(config browserE0Config, caseID string) error { - switch caseID { - case "reject-malformed-request": - return browserE0MalformedRequest(config, caseID) - case "reject-wasm-pin-substitution": - return browserE0StartupFailure(config, caseID, browserE0Overrides{wasmSHA: substitutedBrowserE0Digest(config.wasmPin.sha256)}) - case "reject-steady-without-warmup": - return browserE0SteadyWithoutWarmup(config, caseID) - case "reject-backend-root-substitution": - return browserE0BackendRootSubstitution(config, caseID) - case "reject-browser-engine-pin-substitution": - if err := browserE0EngineSubstitution(config, caseID); err != nil { - return err - } - return browserE0BrowserPinSubstitution(config, caseID+"-launcher") - case "reject-hung-gateway-timeout-and-late-commit": - return browserE0HungGatewayTimeout(config, caseID) - default: - return errors.New("unknown browser E0 hostile case") - } -} - -func browserE0MalformedRequest(config browserE0Config, caseID string) error { - testCase := browserE0Case{backend: "kzg", lifecycle: rq2wire.LifecycleBrowserCold} - harness, gateway, _, err := startBrowserE0Worker(config, testCase, caseID, browserE0Overrides{}) - if err != nil { - return err - } - defer gateway.Close() - defer harness.Abort() - if err := harness.WriteRaw([]byte(`{"schema_version":"malt-rq2-worker-request/v1","unknown":true}`)); err != nil { - return err - } - if err := harness.WaitError(); err != nil { - return err - } - if gateway.Operations() != 0 { - return errors.New("malformed browser request reached the Gateway writer capability") - } - return nil -} - -func browserE0StartupFailure(config browserE0Config, caseID string, overrides browserE0Overrides) error { - testCase := browserE0Case{backend: "kzg", lifecycle: rq2wire.LifecycleBrowserCold} - harness, gateway, _, err := startBrowserE0Worker(config, testCase, caseID, overrides) - if err != nil { - return err - } - defer gateway.Close() - defer harness.Abort() - request := browserE0Request(config.fixture.FixtureID, testCase, caseID, "preflight", rq2wire.RecordPreflight, "", false, "") - var record rq2wire.WorkerRecord - if err := harness.Exchange(request, &record); err == nil { - return fmt.Errorf("artifact-pin substitution unexpectedly emitted a worker record: %+v", record) - } - if err := harness.WaitError(); err != nil { - return err - } - if gateway.Operations() != 0 { - return errors.New("artifact-pin substitution reached the Gateway writer capability") - } - return nil -} - -func browserE0BrowserPinSubstitution(config browserE0Config, caseID string) error { - testCase := browserE0Case{backend: "kzg", lifecycle: rq2wire.LifecycleBrowserCold} - overrides := browserE0Overrides{browserSHA: substitutedBrowserE0Digest(config.browserPin.sha256)} - harness, gateway, _, err := startBrowserE0Worker(config, testCase, caseID, overrides) - if err != nil { - return err - } - defer gateway.Close() - defer harness.Abort() - record, err := browserE0Exchange(harness, browserE0Request(config.fixture.FixtureID, testCase, caseID, "preflight", rq2wire.RecordPreflight, "", false, ""), false) - if err != nil { - return err - } - if record.Success || record.FailureClass != "capability_unavailable" || !strings.Contains(record.Error, "launcher unavailable") { - return fmt.Errorf("browser-launcher pin substitution was not rejected: %+v", record) - } - if gateway.Operations() != 0 { - return errors.New("browser-launcher pin substitution reached the Gateway writer capability") - } - return nil -} - -func browserE0SteadyWithoutWarmup(config browserE0Config, caseID string) error { - testCase := browserE0Case{backend: "kzg", lifecycle: rq2wire.LifecycleBrowserSteady, operations: []browserE0Operation{{name: "document-edit-cid-binding-submit", measured: true}}} - harness, gateway, root, err := startBrowserE0Worker(config, testCase, caseID, browserE0Overrides{}) - if err != nil { - return err - } - defer gateway.Close() - defer harness.Abort() - if _, err := browserE0Exchange(harness, browserE0Request(config.fixture.FixtureID, testCase, caseID, "preflight", rq2wire.RecordPreflight, "", false, ""), true); err != nil { - return err - } - if _, err := browserE0Exchange(harness, browserE0Request(config.fixture.FixtureID, testCase, caseID, "session-start", rq2wire.RecordSessionStart, "", false, root.String()), true); err != nil { - return err - } - record, err := browserE0Exchange(harness, browserE0Request(config.fixture.FixtureID, testCase, caseID, "measured-without-warmup", rq2wire.RecordMutation, "document-edit-cid-binding-submit", true, root.String()), false) - if err != nil { - return err - } - if record.Success || record.FailureClass != "lifecycle_invalid" { - return fmt.Errorf("steady browser accepted a measured mutation without warmup: %+v", record) - } - if gateway.Operations() != 0 { - return errors.New("steady-without-warmup reached the Gateway writer capability") - } - return nil -} - -func browserE0BackendRootSubstitution(config browserE0Config, caseID string) error { - testCase := browserE0Case{backend: "kzg", lifecycle: rq2wire.LifecycleBrowserCold} - harness, gateway, _, err := startBrowserE0Worker(config, testCase, caseID, browserE0Overrides{}) - if err != nil { - return err - } - defer gateway.Close() - defer harness.Abort() - if _, err := browserE0Exchange(harness, browserE0Request(config.fixture.FixtureID, testCase, caseID, "preflight", rq2wire.RecordPreflight, "", false, ""), true); err != nil { - return err - } - wrongRoot, err := config.fixture.Root("ipa") - if err != nil { - return err - } - request := browserE0Request(config.fixture.FixtureID, testCase, caseID, "session-start-substituted-root", rq2wire.RecordSessionStart, "", false, wrongRoot.String()) - var record rq2wire.WorkerRecord - if err := harness.Exchange(request, &record); err == nil { - return fmt.Errorf("backend-root substitution unexpectedly emitted a record: %+v", record) - } - if err := harness.WaitError(); err != nil { - return err - } - if gateway.Operations() != 0 { - return errors.New("backend-root substitution reached the Gateway writer capability") - } - return nil -} - -func browserE0EngineSubstitution(config browserE0Config, caseID string) error { - testCase := browserE0Case{backend: "kzg", lifecycle: rq2wire.LifecycleBrowserCold} - wrongEngine := "chromium-0.0" - if config.browserEngine == wrongEngine { - wrongEngine = "chrome-0.0" - } - harness, gateway, _, err := startBrowserE0Worker(config, testCase, caseID, browserE0Overrides{engine: wrongEngine}) - if err != nil { - return err - } - defer gateway.Close() - defer harness.Abort() - record, err := browserE0Exchange(harness, browserE0Request(config.fixture.FixtureID, testCase, caseID, "preflight", rq2wire.RecordPreflight, "", false, ""), false) - if err != nil { - return err - } - if record.Success || record.FailureClass != "capability_unavailable" || !strings.Contains(record.Error, "registered") { - return fmt.Errorf("browser engine substitution was not rejected: %+v", record) - } - if gateway.Operations() != 0 { - return errors.New("browser-engine substitution reached the Gateway writer capability") - } - return nil -} - -const ( - // The delay exceeds the force-termination bound. After the proxy reports - // that the production request arrived, the test explicitly cancels the CDP - // wait and then waits past this delay plus a grace interval. Forwarding is - // therefore possible only if killing Chromium failed to cancel real work. - browserE0HungGatewayDelay = 7 * time.Second - browserE0RouteReachBound = 5 * time.Second - browserE0LateCommitGrace = 500 * time.Millisecond -) - -// browserE0DelayedGatewayProxy forwards the complete production HTTP surface -// until armed. Once armed, exactly the client-root route is held beyond the -// browser exchange deadline. Cancellation must come from terminating the real -// Chromium process; a delayed request is never forwarded after cancellation. -type browserE0DelayedGatewayProxy struct { - server *httptest.Server - proxy *httputil.ReverseProxy - delay time.Duration - started chan time.Time - - mu sync.Mutex - armed bool - requests uint64 -} - -func newBrowserE0DelayedGatewayProxy(target string, delay time.Duration) (*browserE0DelayedGatewayProxy, error) { - parsed, err := url.Parse(target) - if err != nil || parsed.Scheme == "" || parsed.Host == "" || delay <= 0 { - return nil, errors.New("invalid browser E0 delayed Gateway proxy") - } - value := &browserE0DelayedGatewayProxy{ - proxy: httputil.NewSingleHostReverseProxy(parsed), delay: delay, started: make(chan time.Time, 1), - } - value.server = httptest.NewServer(value) - return value, nil -} - -func (p *browserE0DelayedGatewayProxy) ServeHTTP(response http.ResponseWriter, request *http.Request) { - p.mu.Lock() - armed := p.armed && request.Method == http.MethodPost && request.URL.Path == "/v1/evaluation/authentication/batches" - if armed { - p.requests++ - } - p.mu.Unlock() - if !armed { - p.proxy.ServeHTTP(response, request) - return - } - started := time.Now() - select { - case p.started <- started: - default: - } - timer := time.NewTimer(p.delay) - defer timer.Stop() - select { - case <-request.Context().Done(): - return - case <-timer.C: - if request.Context().Err() != nil { - return - } - p.proxy.ServeHTTP(response, request) - } -} - -func (p *browserE0DelayedGatewayProxy) URL() string { return p.server.URL } - -func (p *browserE0DelayedGatewayProxy) Arm() error { - p.mu.Lock() - defer p.mu.Unlock() - if p.armed || p.requests != 0 { - return errors.New("browser E0 delayed Gateway proxy was reused") - } - p.armed = true - return nil -} - -func (p *browserE0DelayedGatewayProxy) Requests() uint64 { - p.mu.Lock() - defer p.mu.Unlock() - return p.requests -} - -func (p *browserE0DelayedGatewayProxy) Close() { - if p != nil && p.server != nil { - p.server.Close() - p.server = nil - } -} - -func browserE0HungGatewayTimeout(config browserE0Config, caseID string) error { - testCase := browserE0Case{backend: "kzg", lifecycle: rq2wire.LifecycleBrowserSteady} - tokenDigest := sha256.Sum256([]byte("malt-rq2-browser-e0\x00" + caseID)) - token := hex.EncodeToString(tokenDigest[:]) - gateway, root, err := rq2e0.NewConformanceGateway(config.fixture, testCase.backend, token) - if err != nil { - return err - } - defer gateway.Close() - proxy, err := newBrowserE0DelayedGatewayProxy(gateway.URL(), browserE0HungGatewayDelay) - if err != nil { - return err - } - defer proxy.Close() - arguments := browserE0WorkerArguments(config, testCase, caseID, proxy.URL(), token, browserE0Overrides{}) - workerConfig, err := parseFlags(arguments, io.Discard) - if err != nil { - return err - } - worker, err := newBrowserWorker(workerConfig) - if err != nil { - return err - } - defer func() { - if worker.browser != nil { - _ = worker.browser.close() - } - }() - - exchange := func(ctx context.Context, request rq2wire.WorkerRequest) (rq2wire.WorkerRecord, error) { - if err := worker.bindRequest(request); err != nil { - return rq2wire.WorkerRecord{}, err - } - record := worker.exchange(ctx, request) - if err := rq2wire.Bind(record, request); err != nil { - return rq2wire.WorkerRecord{}, err - } - if err := record.Validate(); err != nil { - return rq2wire.WorkerRecord{}, err - } - return record, nil - } - boundedExchange := func(request rq2wire.WorkerRequest) (rq2wire.WorkerRecord, error) { - requestContext, cancel := context.WithTimeout(context.Background(), config.timeout) - defer cancel() - return exchange(requestContext, request) - } - preflight := browserE0Request(config.fixture.FixtureID, testCase, caseID, "preflight", rq2wire.RecordPreflight, "", false, "") - preflightRecord, err := boundedExchange(preflight) - if err != nil || !preflightRecord.Success { - return fmt.Errorf("hung-Gateway preflight: record=%+v err=%w", preflightRecord, err) - } - start := browserE0Request(config.fixture.FixtureID, testCase, caseID, "session-start", rq2wire.RecordSessionStart, "", false, root.String()) - startRecord, err := boundedExchange(start) - if err != nil || !startRecord.Success { - return fmt.Errorf("hung-Gateway session start: record=%+v err=%w", startRecord, err) - } - warmup := browserE0Request(config.fixture.FixtureID, testCase, caseID, "warmup", rq2wire.RecordMutation, "map-replace", false, root.String()) - warmupRecord, err := boundedExchange(warmup) - if err != nil || !warmupRecord.Success || warmupRecord.Mutation == nil { - return fmt.Errorf("hung-Gateway steady warmup: record=%+v err=%w", warmupRecord, err) - } - baselineOperations := gateway.Operations() - if baselineOperations != 1 { - return fmt.Errorf("hung-Gateway warmup accepted %d operations, want 1", baselineOperations) - } - if err := proxy.Arm(); err != nil { - return err - } - originalBrowser := worker.browser - if originalBrowser == nil || originalBrowser.process == nil { - return errors.New("hung-Gateway case has no live Chromium process") - } - request := browserE0Request( - config.fixture.FixtureID, testCase, caseID, "hung-mutation", rq2wire.RecordMutation, - "document-edit-cid-binding-submit", true, warmupRecord.Mutation.ReceiptRoot, - ) - type exchangeResult struct { - record rq2wire.WorkerRecord - err error - } - requestContext, cancel := context.WithTimeout(context.Background(), config.timeout) - result := make(chan exchangeResult, 1) - go func() { - record, err := exchange(requestContext, request) - result <- exchangeResult{record: record, err: err} - }() - var started time.Time - select { - case started = <-proxy.started: - // The request is now held inside the delayed production-route proxy. - // Trigger the same context-failure path as the per-request deadline, - // without racing route arrival against a preselected wall deadline. - cancel() - case premature := <-result: - cancel() - return fmt.Errorf("hung-Gateway exchange completed before the production route was reached: record=%+v err=%w", premature.record, premature.err) - case <-time.After(browserE0RouteReachBound): - cancel() - return errors.New("hung-Gateway mutation did not reach the production client-root route within the safety bound") - } - terminationStarted := time.Now() - var completed exchangeResult - select { - case completed = <-result: - case <-time.After(browserForceTerminationBound + browserE0LateCommitGrace): - return errors.New("hung-Gateway exchange did not complete within the force-termination bound") - } - record, exchangeErr := completed.record, completed.err - if exchangeErr != nil { - return exchangeErr - } - if time.Since(terminationStarted) > browserForceTerminationBound+browserE0LateCommitGrace { - return errors.New("hung-Gateway exchange exceeded the force-termination bound") - } - timeoutText := strings.ToLower(record.Error) - if record.Success || record.FailureClass != "browser_execution" || - !strings.Contains(timeoutText, "deadline") && !strings.Contains(timeoutText, "timeout") && !strings.Contains(timeoutText, "canceled") { - return fmt.Errorf("hung Gateway did not produce a browser deadline failure: %+v", record) - } - if strings.Contains(timeoutText, "force terminate chromium exceeded") { - return fmt.Errorf("hung Gateway exceeded the strict force-termination bound: %+v", record) - } - if worker.browser != nil || worker.state != "poisoned" { - return errors.New("timed-out browser worker retained a reusable Chromium/WASM session") - } - select { - case <-originalBrowser.process.done: - default: - return errors.New("timed-out browser worker returned before Chromium terminated") - } - remaining := browserE0HungGatewayDelay + browserE0LateCommitGrace - time.Since(started) - if remaining > 0 { - timer := time.NewTimer(remaining) - defer timer.Stop() - <-timer.C - } - if proxy.Requests() != 1 || gateway.Operations() != baselineOperations { - return fmt.Errorf("late browser work escaped termination: delayed requests=%d Gateway operations=%d, want 1/%d", proxy.Requests(), gateway.Operations(), baselineOperations) - } - return nil -} - -func substitutedBrowserE0Digest(live string) string { - value := strings.Repeat("0", 64) - if live == value { - return strings.Repeat("1", 64) - } - return value -} - -func shortBrowserE0ID(value string) string { - digest := sha256.Sum256([]byte(value)) - return hex.EncodeToString(digest[:6]) -} - -func browserE0ProfileDigest() string { - contract, err := browserE0Profile.Contract() - if err != nil { - return "" - } - return contract.ProfileSHA256 -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/selftest_test.go b/tools/evaluation/cmd/malt-eval-rq2-browser-worker/selftest_test.go deleted file mode 100644 index 56bfaf2..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-browser-worker/selftest_test.go +++ /dev/null @@ -1,261 +0,0 @@ -package main - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" - "testing" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/e0selftest" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2e0" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture" -) - -func TestBrowserE0ProfileContractIsFrozen(t *testing.T) { - contract, err := browserE0Profile.Contract() - if err != nil { - t.Fatal(err) - } - if contract.ProfileID != browserE0ProfileID || contract.ProfileSHA256 != browserE0ExpectedProfileSHA256 || contract.PositiveCases != 4 || contract.HostileCases != 6 { - t.Fatalf("browser formal E0 contract = %+v", contract) - } - if browserE0ProfileDigest() != browserE0ExpectedProfileSHA256 { - t.Fatal("compiled browser E0 digest helper drifted") - } -} - -func TestBrowserE0FlagsRequireFiveTypedAbsolutePaths(t *testing.T) { - paths := browserE0TestFiles(t) - arguments := browserE0TestArguments(paths) - config, err := parseBrowserE0Flags(arguments, &bytes.Buffer{}) - if err != nil { - t.Fatal(err) - } - if config.timeout != 2*time.Minute || len(config.inputPaths()) != 5 { - t.Fatalf("parsed browser E0 config = %+v", config) - } - for index, argument := range arguments { - if argument == "-wasm-exec" { - hostile := append([]string(nil), arguments...) - hostile[index+1] = "relative/wasm_exec.js" - if _, err := parseBrowserE0Flags(hostile, &bytes.Buffer{}); err == nil { - t.Fatal("relative typed E0 input was accepted") - } - break - } - } - if _, err := parseBrowserE0Flags(arguments[:len(arguments)-4], &bytes.Buffer{}); err == nil { - t.Fatal("browser E0 accepted a missing typed input") - } -} - -func TestBrowserE0TypedFlagsConsumeExactInvocationInputs(t *testing.T) { - paths := browserE0TestFiles(t) - executable, err := os.Executable() - if err != nil { - t.Fatal(err) - } - executable, err = filepath.EvalSymlinks(executable) - if err != nil { - t.Fatal(err) - } - inputs := []e0selftest.InputFile{ - {FileID: "browser-launcher", Path: paths["browser-launcher"]}, - {FileID: "machine-descriptor", Path: paths["machine-descriptor"]}, - {FileID: "rq2-source-fixture", Path: paths["fixture"]}, - {FileID: "wasm", Path: paths["wasm"]}, - {FileID: "wasm-exec", Path: paths["wasm-exec"]}, - } - invocation, err := e0selftest.BuildInvocationEnvironment( - browserE0CapabilityID, browserE0Profile, - e0selftest.InputFile{FileID: "rq2-browser-worker", Path: executable}, - inputs, []string{"rq2-source-fixture"}, - ) - if err != nil { - t.Fatal(err) - } - t.Setenv(e0selftest.InvocationEnvironment, invocation) - config, err := parseBrowserE0Flags(browserE0TestArguments(paths), &bytes.Buffer{}) - if err != nil { - t.Fatal(err) - } - if err := validateBrowserE0InvocationPaths(config.inputPaths()); err != nil { - t.Fatal(err) - } - config.wasmExecPath = config.wasmPath - if err := validateBrowserE0InvocationPaths(config.inputPaths()); err == nil { - t.Fatal("browser formal E0 accepted typed flags that did not consume the exact invocation inputs") - } -} - -func TestBrowserFormalE0ExecutesPinnedChromiumAndWASM(t *testing.T) { - if testing.Short() || runtime.GOOS != "linux" { - t.Skip("formal Chromium/WASM E0 integration requires a non-short Linux test") - } - directory := t.TempDir() - browserPath := os.Getenv("MALT_BROWSER_E0_BROWSER") - var err error - if browserPath == "" { - browserPath, err = exec.LookPath("chromium") - if err != nil { - browserPath, err = exec.LookPath("google-chrome") - } - } - if err != nil || browserPath == "" { - t.Skip("Chromium-family browser is not installed") - } - browserPath, err = filepath.Abs(browserPath) - if err != nil { - t.Fatal(err) - } - browserInfo, err := os.Lstat(browserPath) - if err != nil { - t.Fatal(err) - } - if browserInfo.Mode()&os.ModeSymlink != 0 { - if browserPath != "/snap/bin/chromium" { - t.Skip("formal E0 requires MALT_BROWSER_E0_BROWSER to name a regular non-symlink launcher") - } - // Ubuntu's Chromium snap exposes only a symlink to the generic snap - // dispatcher. Pin a regular launcher script while the runtime version - // probe below independently binds the Chromium build it dispatches. - browserPath = filepath.Join(directory, "chromium-launcher") - if err := os.WriteFile(browserPath, []byte("#!/bin/sh\nexec /snap/bin/chromium \"$@\"\n"), 0o700); err != nil { - t.Fatal(err) - } - } else { - browserPath, err = filepath.EvalSymlinks(browserPath) - if err != nil { - t.Fatal(err) - } - } - wasmExecPath, err := filepath.EvalSymlinks(filepath.Join(runtime.GOROOT(), "lib", "wasm", "wasm_exec.js")) - if err != nil { - t.Skipf("matching wasm_exec.js is unavailable: %v", err) - } - wasmPath := filepath.Join(directory, "writer.wasm") - moduleRoot := filepath.Clean(filepath.Join("..", "..", "..", "..")) - build := exec.Command("go", "build", "-p=6", "-buildvcs=false", "-o", wasmPath, "./tools/evaluation/cmd/malt-eval-rq2-browser-wasm") - build.Dir = moduleRoot - build.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", "CGO_ENABLED=0") - if output, err := build.CombinedOutput(); err != nil { - t.Fatalf("build real Go/WASM writer: %v\n%s", err, output) - } - fixture := browserE0Fixture(t) - fixtureRaw, err := json.Marshal(fixture) - if err != nil { - t.Fatal(err) - } - fixturePath := filepath.Join(directory, "fixture.json") - if err := os.WriteFile(fixturePath, append(fixtureRaw, '\n'), 0o600); err != nil { - t.Fatal(err) - } - machinePin := testMachinePin(t, false) - executable, err := os.Executable() - if err != nil { - t.Fatal(err) - } - executable, err = filepath.EvalSymlinks(executable) - if err != nil { - t.Fatal(err) - } - inputs := []e0selftest.InputFile{ - {FileID: "browser-launcher", Path: browserPath}, - {FileID: "machine-descriptor", Path: machinePin.path}, - {FileID: "rq2-source-fixture", Path: fixturePath}, - {FileID: "wasm", Path: wasmPath}, - {FileID: "wasm-exec", Path: wasmExecPath}, - } - invocation, err := e0selftest.BuildInvocationEnvironment( - browserE0CapabilityID, browserE0Profile, - e0selftest.InputFile{FileID: "rq2-browser-worker", Path: executable}, - inputs, []string{"rq2-source-fixture"}, - ) - if err != nil { - t.Fatal(err) - } - t.Setenv(e0selftest.InvocationEnvironment, invocation) - arguments := []string{ - "-fixture", fixturePath, "-machine-descriptor", machinePin.path, - "-browser-launcher", browserPath, "-wasm", wasmPath, "-wasm-exec", wasmExecPath, - "-request-timeout", "3m", - } - var output bytes.Buffer - if err := runBrowserE0SelfTest(arguments, &output, &bytes.Buffer{}); err != nil { - t.Fatal(err) - } - var receipt e0selftest.Receipt - if err := strictJSON(output.Bytes(), &receipt); err != nil { - t.Fatal(err) - } - if err := receipt.Validate(); err != nil { - t.Fatal(err) - } - if receipt.CapabilityID != browserE0CapabilityID || receipt.ProfileSHA256 != browserE0ExpectedProfileSHA256 || len(receipt.TestedInputs) != 5 { - t.Fatalf("formal browser E0 receipt = %+v", receipt) - } -} - -func browserE0Fixture(t *testing.T) *rq2fixture.Fixture { - t.Helper() - chunkA := make([]byte, 32) - chunkB := make([]byte, 32) - copy(chunkA, "chunk-a") - copy(chunkB, "chunk-b") - listIndex := uint64(0) - seed := sha256.Sum256([]byte("formal browser E0 fixture seed")) - source := &rq2fixture.SourceDefinition{ - SchemaVersion: rq2fixture.SourceSchemaVersion, FixtureID: "formal-browser-e0", MutationSeedSHA256: hex.EncodeToString(seed[:]), - DirectFiles: []rq2fixture.SourceDirectFile{{Path: "document.txt", Coordinate: "document.txt", Bytes: []byte("document")}}, - ListFiles: []rq2fixture.SourceListFile{{ - Path: "list.bin", Coordinate: "list.bin", ChunkSize: 32, TotalSize: 64, - Chunks: []rq2fixture.SourceListChunk{{Index: 0, Bytes: chunkA}, {Index: 1, Bytes: chunkB}}, - }}, - Operations: []rq2fixture.Operation{ - {Name: "document-edit-cid-binding-submit", Kind: rq2fixture.KindDocumentEdit, SourcePath: "document.txt", SourceCoordinate: "document.txt", PayloadBytes: 32}, - {Name: "map-replace", Kind: rq2fixture.KindDirectReplace, SourcePath: "document.txt", SourceCoordinate: "document.txt", PayloadBytes: 16}, - {Name: "map-insert", Kind: rq2fixture.KindDirectInsert, DestinationPath: "inserted.txt", DestinationCoordinate: "inserted.txt", PayloadBytes: 16}, - {Name: "list-append", Kind: rq2fixture.KindListAppend, SourcePath: "list.bin", SourceCoordinate: "list.bin", PayloadBytes: 32}, - {Name: "list-replace", Kind: rq2fixture.KindListReplace, SourcePath: "list.bin", SourceCoordinate: "list.bin", PayloadBytes: 32, ListIndex: &listIndex}, - }, - } - fixture, err := rq2e0.BuildFixture(context.Background(), source) - if err != nil { - t.Fatal(err) - } - return fixture -} - -func browserE0TestFiles(t *testing.T) map[string]string { - t.Helper() - directory := t.TempDir() - paths := map[string]string{} - for index, name := range []string{"fixture", "machine-descriptor", "browser-launcher", "wasm", "wasm-exec"} { - path := filepath.Join(directory, name) - mode := os.FileMode(0o600) - if name == "browser-launcher" { - mode = 0o700 - } - if err := os.WriteFile(path, []byte(name+"-"+strconv.Itoa(index)), mode); err != nil { - t.Fatal(err) - } - paths[name] = path - } - return paths -} - -func browserE0TestArguments(paths map[string]string) []string { - return []string{ - "-fixture", paths["fixture"], "-machine-descriptor", paths["machine-descriptor"], - "-browser-launcher", paths["browser-launcher"], "-wasm", paths["wasm"], - "-wasm-exec", paths["wasm-exec"], "-request-timeout", "2m", - } -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-fixture-build/main.go b/tools/evaluation/cmd/malt-eval-rq2-fixture-build/main.go deleted file mode 100644 index 1ecaccc..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-fixture-build/main.go +++ /dev/null @@ -1,423 +0,0 @@ -// Command malt-eval-rq2-fixture-build constructs the exact shared native and -// browser source fixture for paper RQ2. It computes KZG and IPA complete roots -// from declared bytes, uploads every payload, bootstraps each disposable -// Gateway through the secret evaluation capability, re-fetches and verifies -// both authentication graphs, and only then atomically publishes one strict fixture. -package main - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "net" - "net/http" - "net/url" - "os" - "path/filepath" - "sort" - "strings" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/authenticationgraph" - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture" - "github.com/dewebprotocol/malt-client/transport" - "github.com/dewebprotocol/malt-core/auth/commitment/ipa" - "github.com/dewebprotocol/malt-core/auth/commitment/kzg" - "github.com/dewebprotocol/malt-core/engine" - "github.com/dewebprotocol/malt-core/protocol" - cid "github.com/ipfs/go-cid" -) - -const ( - requestTimeout = 30 * time.Minute - maxSourceBytes = 96 << 20 -) - -type gatewayRegistration struct { - backend string - baseURL string - instanceToken string - bootstrapToken string -} - -type builtBackend struct { - backend string - root cid.Cid - engine *engine.Engine - objects []protocol.AuthenticationCandidate -} - -type artifactDescriptor struct { - SchemaVersion string `json:"schema_version"` - Path string `json:"path"` - SHA256 string `json:"sha256"` - Bytes int64 `json:"bytes"` - InitialRoots []rq2fixture.RootBinding `json:"initial_roots"` -} - -func main() { - if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil { - fmt.Fprintln(os.Stderr, "malt-eval-rq2-fixture-build:", err) - os.Exit(2) - } -} - -func run(args []string, stdout, stderr io.Writer) error { - flags := flag.NewFlagSet("malt-eval-rq2-fixture-build", flag.ContinueOnError) - flags.SetOutput(stderr) - sourcePath := flags.String("source", "", "strict RQ2 source definition JSON") - outputPath := flags.String("out", "", "new absolute fixture JSON path") - kzgBase := flags.String("kzg-base-url", "", "empty disposable KZG Gateway origin") - kzgInstance := flags.String("kzg-gateway-instance-token", "", "KZG Gateway instance identity") - kzgBootstrap := flags.String("kzg-bootstrap-authorization-token", "", "distinct KZG bootstrap capability") - ipaBase := flags.String("ipa-base-url", "", "empty disposable IPA Gateway origin") - ipaInstance := flags.String("ipa-gateway-instance-token", "", "IPA Gateway instance identity") - ipaBootstrap := flags.String("ipa-bootstrap-authorization-token", "", "distinct IPA bootstrap capability") - if err := flags.Parse(args); err != nil { - return err - } - if flags.NArg() != 0 || *sourcePath == "" || *outputPath == "" { - return fmt.Errorf("source, output, and both KZG/IPA Gateway registrations are required") - } - registrations := []gatewayRegistration{ - {backend: "kzg", baseURL: *kzgBase, instanceToken: *kzgInstance, bootstrapToken: *kzgBootstrap}, - {backend: "ipa", baseURL: *ipaBase, instanceToken: *ipaInstance, bootstrapToken: *ipaBootstrap}, - } - for index := range registrations { - registration, err := validateRegistration(registrations[index]) - if err != nil { - return err - } - registrations[index] = registration - } - if err := validateIndependentRegistrations(registrations); err != nil { - return err - } - if registrations[0].baseURL == registrations[1].baseURL { - return fmt.Errorf("KZG and IPA fixture build requires two independently identified disposable Gateways") - } - sourceRaw, err := readPinnedSource(*sourcePath) - if err != nil { - return err - } - source, err := rq2fixture.DecodeSource(sourceRaw) - if err != nil { - return err - } - ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) - defer cancel() - kzgBuild, err := buildBackend(ctx, "kzg", source) - if err != nil { - return err - } - ipaBuild, err := buildBackend(ctx, "ipa", source) - if err != nil { - return err - } - roots := []rq2fixture.RootBinding{{Backend: "kzg", CID: kzgBuild.root.String()}, {Backend: "ipa", CID: ipaBuild.root.String()}} - fixture, err := source.Fixture(roots) - if err != nil { - return err - } - blocks, err := sourceBlocks(source) - if err != nil { - return err - } - for index, build := range []*builtBackend{kzgBuild, ipaBuild} { - if err := initializeGateway(ctx, registrations[index], build, fixture, blocks); err != nil { - return fmt.Errorf("initialize %s Gateway: %w", build.backend, err) - } - } - raw, err := json.Marshal(fixture) - if err != nil { - return err - } - raw = append(raw, '\n') - if _, err := rq2fixture.Decode(raw); err != nil { - return fmt.Errorf("self-verify final RQ2 fixture: %w", err) - } - output, err := filepath.Abs(*outputPath) - if err != nil { - return err - } - if err := publishAtomicExclusive(output, raw); err != nil { - return err - } - digest := sha256.Sum256(raw) - encoder := json.NewEncoder(stdout) - encoder.SetEscapeHTML(false) - return encoder.Encode(artifactDescriptor{ - SchemaVersion: "malt-rq2-source-fixture-artifact/v1", Path: output, - SHA256: hex.EncodeToString(digest[:]), Bytes: int64(len(raw)), InitialRoots: roots, - }) -} - -func validateRegistration(value gatewayRegistration) (gatewayRegistration, error) { - value.baseURL = strings.TrimRight(strings.TrimSpace(value.baseURL), "/") - value.instanceToken, value.bootstrapToken = strings.TrimSpace(value.instanceToken), strings.TrimSpace(value.bootstrapToken) - parsed, err := url.Parse(value.baseURL) - if err != nil || parsed.Host == "" || parsed.User != nil || parsed.Opaque != "" || parsed.Path != "" || parsed.RawPath != "" || parsed.RawQuery != "" || parsed.Fragment != "" || - parsed.Scheme != "https" && !(parsed.Scheme == "http" && loopbackHost(parsed.Hostname())) { - return gatewayRegistration{}, fmt.Errorf("%s Gateway bootstrap origin must be HTTPS or loopback HTTP", value.backend) - } - if !canonicalToken(value.instanceToken) || !canonicalToken(value.bootstrapToken) || value.instanceToken == value.bootstrapToken { - return gatewayRegistration{}, fmt.Errorf("%s Gateway instance/bootstrap tokens must be distinct canonical SHA-256 values", value.backend) - } - return value, nil -} - -func validateIndependentRegistrations(values []gatewayRegistration) error { - seen := make(map[string]string, len(values)*2) - for _, value := range values { - for _, credential := range []struct { - role string - token string - }{{role: "instance", token: value.instanceToken}, {role: "bootstrap", token: value.bootstrapToken}} { - label := value.backend + " " + credential.role - token := credential.token - if previous, exists := seen[token]; exists { - return fmt.Errorf("KZG and IPA fixture build requires four globally distinct Gateway tokens: %s reuses %s token", label, previous) - } - seen[token] = label - } - } - return nil -} - -func loopbackHost(host string) bool { - if strings.EqualFold(host, "localhost") { - return true - } - address := net.ParseIP(host) - return address != nil && address.IsLoopback() -} - -func buildBackend(ctx context.Context, backend string, source *rq2fixture.SourceDefinition) (*builtBackend, error) { - var scheme engine.Profile - var err error - switch backend { - case "kzg": - scheme, err = kzg.NewScheme() - case "ipa": - scheme, err = ipa.NewScheme() - default: - return nil, fmt.Errorf("unsupported backend %q", backend) - } - if err != nil { - return nil, err - } - profiles := engine.NewRegistry() - if err := profiles.Register(scheme); err != nil { - return nil, err - } - e := engine.New(profiles) - candidates, err := source.Candidates(ctx, e, backend) - if err != nil { - return nil, err - } - root, err := cid.Parse(candidates[len(candidates)-1].Root) - if err != nil { - return nil, err - } - return &builtBackend{backend: backend, root: root, engine: e, objects: candidates}, nil -} - -func sourceBlocks(source *rq2fixture.SourceDefinition) ([]transport.Block, error) { - byCID := make(map[string]transport.Block) - add := func(data []byte) error { - key, err := rawCID(data) - if err != nil { - return err - } - byCID[key.KeyString()] = transport.Block{Codec: cid.Raw, Data: append([]byte(nil), data...)} - return nil - } - for _, file := range source.DirectFiles { - if err := add(file.Bytes); err != nil { - return nil, err - } - } - for _, file := range source.ListFiles { - for _, chunk := range file.Chunks { - if err := add(chunk.Bytes); err != nil { - return nil, err - } - } - } - keys := make([]string, 0, len(byCID)) - for key := range byCID { - keys = append(keys, key) - } - sort.Strings(keys) - blocks := make([]transport.Block, len(keys)) - for index, key := range keys { - blocks[index] = byCID[key] - } - return blocks, nil -} - -func initializeGateway(ctx context.Context, registration gatewayRegistration, build *builtBackend, fixture *rq2fixture.Fixture, blocks []transport.Block) error { - evaluation, err := gatewaytransport.New(gatewaytransport.Options{ - BaseURL: registration.baseURL, InstanceToken: registration.instanceToken, HTTPClient: &http.Client{Timeout: requestTimeout}, - }) - if err != nil { - return err - } - remote, err := transport.New(transport.Options{BaseURL: registration.baseURL, HTTPClient: evaluation.InstanceHTTPClient()}) - if err != nil { - return err - } - health, err := evaluation.Health(ctx) - if err != nil { - return err - } - if health.Status != "ok" || health.EvaluationInstanceToken != registration.instanceToken || health.BlobBackend != "embedded" || health.ArcTableMode != "versioned" || - health.CommitmentBackends != "ipa,kzg" || health.AuthenticationExactAcceptance != "true" || health.EvaluationAuthenticationBootstrap != gatewaytransport.BootstrapProfile { - return fmt.Errorf("Gateway health does not expose the exact clean RQ2 bootstrap boundary") - } - for offset := 0; offset < len(blocks); offset += transport.MaxCASBatchBlocks { - part := blocks[offset:min(len(blocks), offset+transport.MaxCASBatchBlocks)] - results, err := remote.PutBatch(ctx, part) - if err != nil { - return fmt.Errorf("upload RQ2 source blocks: %w", err) - } - if len(results) != len(part) { - return fmt.Errorf("source upload result count differs") - } - for i, block := range part { - expected, err := rawCID(block.Data) - if err != nil || !results[i].CID.Equals(expected) { - return fmt.Errorf("source upload returned a mismatched CID") - } - } - } - for i, candidate := range build.objects { - result, err := evaluation.BootstrapEvaluationObject(ctx, registration.bootstrapToken, gatewaytransport.BootstrapObject{OperationID: fmt.Sprintf("rq2-%s-bootstrap-%03d", build.backend, i), Candidate: candidate}) - if err != nil { - return fmt.Errorf("bootstrap candidate %d: %w", i, err) - } - if result.Root.String() != candidate.Root { - return fmt.Errorf("bootstrap candidate %d returned an unexpected Root", i) - } - } - session, err := authenticationgraph.New(evaluation, build.engine) - if err != nil { - return err - } - if _, err := session.Load(ctx, build.root); err != nil { - return fmt.Errorf("import bootstrapped authentication graph: %w", err) - } - view, err := session.Snapshot() - if err != nil { - return err - } - if err := fixture.ValidateInitialGraph(view, build.backend); err != nil { - return fmt.Errorf("bootstrapped source/root oracle: %w", err) - } - - return nil -} - -func rawCID(data []byte) (cid.Cid, error) { - digest, err := cid.Prefix{Version: 1, Codec: cid.Raw, MhType: 0x12, MhLength: 32}.Sum(data) - return digest, err -} - -func readPinnedSource(path string) ([]byte, error) { - lstat, err := os.Lstat(path) - if err != nil || lstat.Mode()&os.ModeSymlink != 0 || !lstat.Mode().IsRegular() || lstat.Size() <= 0 || lstat.Size() > maxSourceBytes { - return nil, fmt.Errorf("RQ2 source definition is not a bounded regular file: %v", err) - } - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() - opened, err := file.Stat() - if err != nil || !os.SameFile(lstat, opened) { - return nil, fmt.Errorf("RQ2 source definition changed before it was opened") - } - raw, err := io.ReadAll(io.LimitReader(file, maxSourceBytes+1)) - if err != nil { - return nil, err - } - post, err := os.Lstat(path) - if err != nil || !os.SameFile(lstat, post) || int64(len(raw)) != lstat.Size() { - return nil, fmt.Errorf("RQ2 source definition changed while it was read") - } - return raw, nil -} - -func publishAtomicExclusive(path string, raw []byte) error { - if !filepath.IsAbs(path) { - return fmt.Errorf("fixture output path must be absolute") - } - directory := filepath.Dir(path) - if err := os.MkdirAll(directory, 0o700); err != nil { - return err - } - if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) { - if err == nil { - return fmt.Errorf("fixture output already exists") - } - return err - } - temporary, err := os.CreateTemp(directory, ".rq2-fixture-*.tmp") - if err != nil { - return err - } - temporaryPath := temporary.Name() - succeeded := false - defer func() { - _ = temporary.Close() - if !succeeded { - _ = os.Remove(temporaryPath) - } - }() - if err := temporary.Chmod(0o600); err != nil { - return err - } - if _, err := temporary.Write(raw); err != nil { - return err - } - if err := temporary.Sync(); err != nil { - return err - } - if err := temporary.Close(); err != nil { - return err - } - if err := os.Link(temporaryPath, path); err != nil { - return err - } - if err := os.Remove(temporaryPath); err != nil { - _ = os.Remove(path) - return err - } - directoryHandle, err := os.Open(directory) - if err != nil { - _ = os.Remove(path) - return err - } - syncErr := directoryHandle.Sync() - closeErr := directoryHandle.Close() - if syncErr != nil || closeErr != nil { - _ = os.Remove(path) - return errors.Join(syncErr, closeErr) - } - succeeded = true - return nil -} - -func canonicalToken(value string) bool { - if len(value) != sha256.Size*2 { - return false - } - decoded, err := hex.DecodeString(value) - return err == nil && hex.EncodeToString(decoded) == value -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-fixture-build/main_test.go b/tools/evaluation/cmd/malt-eval-rq2-fixture-build/main_test.go deleted file mode 100644 index e00d41e..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-fixture-build/main_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package main - -import ( - "crypto/sha256" - "encoding/hex" - "os" - "path/filepath" - "testing" - - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture" - "github.com/dewebprotocol/malt-core/maltcid" -) - -func TestBuildsMatchedKZGAndIPAFixtureFromDeclaredBytes(t *testing.T) { - chunkA := make([]byte, 32) - chunkB := make([]byte, 32) - copy(chunkA, "chunk-a") - copy(chunkB, "chunk-b") - seed := sha256.Sum256([]byte("builder seed")) - source := &rq2fixture.SourceDefinition{ - SchemaVersion: rq2fixture.SourceSchemaVersion, FixtureID: "fixture", MutationSeedSHA256: hex.EncodeToString(seed[:]), - DirectFiles: []rq2fixture.SourceDirectFile{{Path: "document.txt", Coordinate: "document.txt", Bytes: []byte("document")}}, - ListFiles: []rq2fixture.SourceListFile{{ - Path: "list.bin", Coordinate: "list.bin", ChunkSize: 32, TotalSize: 64, - Chunks: []rq2fixture.SourceListChunk{{Index: 0, Bytes: chunkA}, {Index: 1, Bytes: chunkB}}, - }}, - Operations: []rq2fixture.Operation{{ - Name: "document-edit-cid-binding-submit", Kind: rq2fixture.KindDocumentEdit, - SourcePath: "document.txt", SourceCoordinate: "document.txt", PayloadBytes: 32, - }, { - Name: "list-append", Kind: rq2fixture.KindListAppend, - SourcePath: "list.bin", SourceCoordinate: "list.bin", PayloadBytes: 32, - }}, - } - if err := source.Validate(); err != nil { - t.Fatal(err) - } - kzgBuild, err := buildBackend(t.Context(), "kzg", source) - if err != nil { - t.Fatal(err) - } - ipaBuild, err := buildBackend(t.Context(), "ipa", source) - if err != nil { - t.Fatal(err) - } - kzgDescriptor, _, kzgErr := maltcid.ParseRoot(kzgBuild.root) - ipaDescriptor, _, ipaErr := maltcid.ParseRoot(ipaBuild.root) - if kzgErr != nil || ipaErr != nil || kzgDescriptor.Profile != maltcid.KZG4096 || ipaDescriptor.Profile != maltcid.IPA256 || kzgBuild.root.Equals(ipaBuild.root) { - t.Fatalf("backend roots = %s, %s", kzgBuild.root, ipaBuild.root) - } - if len(kzgBuild.objects) != 2 || len(ipaBuild.objects) != 2 { - t.Fatalf("complete bootstrap objects = %d, %d", len(kzgBuild.objects), len(ipaBuild.objects)) - } - fixture, err := source.Fixture([]rq2fixture.RootBinding{{Backend: "kzg", CID: kzgBuild.root.String()}, {Backend: "ipa", CID: ipaBuild.root.String()}}) - if err != nil { - t.Fatal(err) - } - if len(fixture.InitialRoots) != 2 || len(fixture.DirectFiles) != 1 || len(fixture.ListFiles[0].Chunks) != 2 { - t.Fatalf("built fixture = %#v", fixture) - } - blocks, err := sourceBlocks(source) - if err != nil || len(blocks) != 3 { - t.Fatalf("deduplicated source blocks = %d, %v", len(blocks), err) - } -} - -func TestFixturePublishIsAtomicAndExclusive(t *testing.T) { - path := filepath.Join(t.TempDir(), "fixture.json") - if err := publishAtomicExclusive(path, []byte("first\n")); err != nil { - t.Fatal(err) - } - if err := publishAtomicExclusive(path, []byte("second\n")); err == nil { - t.Fatal("exclusive fixture publication overwrote an existing artifact") - } - data, err := os.ReadFile(path) - if err != nil || string(data) != "first\n" { - t.Fatalf("published fixture = %q, %v", data, err) - } -} - -func TestBootstrapRegistrationRequiresProtectedOriginAndDistinctSecrets(t *testing.T) { - valid := gatewayRegistration{ - backend: "kzg", baseURL: "http://127.0.0.1:8080", - instanceToken: stringsRepeat("a"), bootstrapToken: stringsRepeat("b"), - } - if _, err := validateRegistration(valid); err != nil { - t.Fatal(err) - } - hostile := valid - hostile.baseURL = "http://192.0.2.1:8080" - if _, err := validateRegistration(hostile); err == nil { - t.Fatal("non-loopback plaintext bootstrap origin was accepted") - } - hostile = valid - hostile.bootstrapToken = hostile.instanceToken - if _, err := validateRegistration(hostile); err == nil { - t.Fatal("shared instance/bootstrap secret was accepted") - } - - kzg, err := validateRegistration(valid) - if err != nil { - t.Fatal(err) - } - ipa, err := validateRegistration(gatewayRegistration{ - backend: "ipa", baseURL: "http://127.0.0.1:8081", - instanceToken: stringsRepeat("c"), bootstrapToken: stringsRepeat("d"), - }) - if err != nil { - t.Fatal(err) - } - if err := validateIndependentRegistrations([]gatewayRegistration{kzg, ipa}); err != nil { - t.Fatalf("independent registrations: %v", err) - } - for name, mutate := range map[string]func(*gatewayRegistration){ - "ipa instance reuses kzg bootstrap": func(value *gatewayRegistration) { value.instanceToken = kzg.bootstrapToken }, - "ipa bootstrap reuses kzg instance": func(value *gatewayRegistration) { value.bootstrapToken = kzg.instanceToken }, - } { - t.Run(name, func(t *testing.T) { - hostileIPA := ipa - mutate(&hostileIPA) - if err := validateIndependentRegistrations([]gatewayRegistration{kzg, hostileIPA}); err == nil { - t.Fatal("cross-role Gateway token reuse was accepted") - } - }) - } -} - -func stringsRepeat(value string) string { - result := "" - for range 64 { - result += value - } - return result -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-worker/main.go b/tools/evaluation/cmd/malt-eval-rq2-worker/main.go deleted file mode 100644 index 3655301..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-worker/main.go +++ /dev/null @@ -1,407 +0,0 @@ -// Command malt-eval-rq2-worker is the native client-root process boundary for -// the paper RQ2 evaluator. Stdout is JSONL only. One process owns one complete -// long-lived session and exits after a verified session-end audit. -package main - -import ( - "bufio" - "bytes" - "context" - "encoding/json" - "errors" - "flag" - "fmt" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2wire" - "io" - "net/http" - "os" - "regexp" - "runtime" - "strconv" - "strings" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-client/internal/evaluation/machine" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture" - "github.com/dewebprotocol/malt-client/transport" -) - -const ( - maxWorkerLineBytes = 1 << 20 - maxFixtureBytes = 64 << 20 - maxRequests = 2048 -) - -var identityPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$`) - -type workerConfig struct { - gatewayBaseURL string - fixturePath string - workerID string - platformID string - clientKind string - backend string - lifecycle string - requestTimeout time.Duration - lowPowerARM bool - gatewayToken string - machinePath string - machineSHA256 string - machineBytes int64 - machine *machine.Evidence -} - -type worker struct { - config workerConfig - fixture *rq2fixture.Fixture - remote *transport.Client - evaluation *gatewaytransport.Client - native *nativeSession - sessionID string - fixtureID string - state string - receiptCount uint64 -} - -func main() { - var err error - if len(os.Args) > 1 && os.Args[1] == "--e0-self-test" { - err = runNativeE0SelfTest(os.Args[2:], os.Stdout, os.Stderr) - } else { - err = run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr) - } - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func run(args []string, stdin io.Reader, stdout, stderr io.Writer) error { - config, err := parseFlags(args, stderr) - if err != nil { - return err - } - fixture, err := readFixture(config.fixturePath) - if err != nil { - return err - } - evaluation, err := gatewaytransport.New(gatewaytransport.Options{ - BaseURL: config.gatewayBaseURL, InstanceToken: config.gatewayToken, HTTPClient: &http.Client{Timeout: config.requestTimeout}, - }) - if err != nil { - return err - } - remote, err := transport.New(transport.Options{BaseURL: config.gatewayBaseURL, HTTPClient: evaluation.InstanceHTTPClient()}) - if err != nil { - return err - } - healthContext, cancelHealth := context.WithTimeout(context.Background(), config.requestTimeout) - health, err := evaluation.Health(healthContext) - cancelHealth() - if err != nil { - return fmt.Errorf("verify disposable Gateway instance health: %w", err) - } - if health.Status != "ok" || health.EvaluationInstanceToken != config.gatewayToken { - return fmt.Errorf("Gateway health did not echo the exact registered disposable instance token") - } - machineEvidence, err := machine.LoadPinned(config.machinePath, config.machineSHA256, config.machineBytes) - if err != nil { - return fmt.Errorf("verify paper machine descriptor: %w", err) - } - if machineEvidence.Descriptor.LowPowerARM() != config.lowPowerARM { - return fmt.Errorf("-low-power-arm does not match the verified machine descriptor classification") - } - config.machine = machineEvidence - sourceFixture, err := rq2fixture.Decode(fixture) - if err != nil { - return err - } - w := &worker{config: config, fixture: sourceFixture, remote: remote, evaluation: evaluation, state: rq2wire.RecordPreflight} - defer func() { - if w.native != nil { - _ = w.native.close() - } - }() - encoder := json.NewEncoder(stdout) - encoder.SetEscapeHTML(false) - scanner := bufio.NewScanner(stdin) - scanner.Buffer(make([]byte, 64<<10), maxWorkerLineBytes) - requests := 0 - for scanner.Scan() { - requests++ - if requests > maxRequests { - return fmt.Errorf("RQ2 worker exceeds %d-request session limit", maxRequests) - } - request, err := decodeWorkerRequest(scanner.Bytes()) - if err != nil { - return err - } - if err := w.bindRequest(request); err != nil { - return err - } - record := w.exchange(request) - if err := encoder.Encode(record); err != nil { - return err - } - if request.RecordKind == rq2wire.RecordSessionEnd { - if scanner.Scan() { - return fmt.Errorf("RQ2 worker received input after session-end") - } - if err := scanner.Err(); err != nil { - return err - } - return nil - } - } - if err := scanner.Err(); err != nil { - if strings.Contains(err.Error(), "token too long") { - return fmt.Errorf("RQ2 worker request exceeds %d bytes", maxWorkerLineBytes) - } - return err - } - return fmt.Errorf("RQ2 worker input closed before session-end") -} - -func parseFlags(args []string, stderr io.Writer) (workerConfig, error) { - flags := flag.NewFlagSet("malt-eval-rq2-worker", flag.ContinueOnError) - flags.SetOutput(stderr) - baseURL := flags.String("gateway-base-url", "", "Gateway base URL") - fixture := flags.String("fixture", "", "pinned logical fixture file") - workerID := flags.String("worker-id", "", "registered worker identifier") - platformID := flags.String("platform-id", "", "registered platform identifier") - clientKind := flags.String("client-kind", "", "native or browser-wasm") - backend := flags.String("backend", "", "kzg or ipa") - lifecycle := flags.String("lifecycle", "", "registered lifecycle") - lowPowerARM := flags.String("low-power-arm", "", "registered low-power ARM classification (true or false)") - gatewayToken := flags.String("gateway-instance-token", "", "campaign-pinned disposable Gateway instance token") - machinePath := flags.String("machine-descriptor", "", "pinned machine descriptor artifact") - machineSHA256 := flags.String("machine-descriptor-sha256", "", "registered machine descriptor SHA-256") - machineBytes := flags.Int64("machine-descriptor-bytes", 0, "registered machine descriptor byte length") - timeout := flags.Duration("request-timeout", 5*time.Minute, "per-request timeout") - if err := flags.Parse(args); err != nil { - return workerConfig{}, err - } - if flags.NArg() != 0 { - return workerConfig{}, fmt.Errorf("unexpected positional arguments") - } - config := workerConfig{ - gatewayBaseURL: strings.TrimSpace(*baseURL), fixturePath: strings.TrimSpace(*fixture), - workerID: strings.TrimSpace(*workerID), platformID: strings.TrimSpace(*platformID), - clientKind: strings.TrimSpace(*clientKind), backend: strings.TrimSpace(*backend), - lifecycle: strings.TrimSpace(*lifecycle), requestTimeout: *timeout, - gatewayToken: strings.TrimSpace(*gatewayToken), - machinePath: strings.TrimSpace(*machinePath), machineSHA256: strings.TrimSpace(*machineSHA256), machineBytes: *machineBytes, - } - if *lowPowerARM != "true" && *lowPowerARM != "false" { - return workerConfig{}, fmt.Errorf("-low-power-arm must be explicitly true or false") - } - config.lowPowerARM, _ = strconv.ParseBool(*lowPowerARM) - if !canonicalLowerSHA256(config.gatewayToken) { - return workerConfig{}, fmt.Errorf("-gateway-instance-token must be one canonical lowercase SHA-256 value") - } - if config.machinePath == "" || !canonicalLowerSHA256(config.machineSHA256) || config.machineBytes <= 0 || config.machineBytes > machine.MaxDescriptorBytes { - return workerConfig{}, fmt.Errorf("machine descriptor path/SHA-256/bytes pin is required") - } - if config.gatewayBaseURL == "" || config.fixturePath == "" || !identityPattern.MatchString(config.workerID) || !identityPattern.MatchString(config.platformID) { - return workerConfig{}, fmt.Errorf("gateway, fixture, worker, and platform flags are required") - } - if config.backend != "kzg" && config.backend != "ipa" { - return workerConfig{}, fmt.Errorf("-backend must be kzg or ipa") - } - if config.clientKind == rq2wire.ClientNative && config.lifecycle != rq2wire.LifecycleNativeLong || - config.clientKind == rq2wire.ClientBrowserWASM && config.lifecycle != rq2wire.LifecycleBrowserCold && config.lifecycle != rq2wire.LifecycleBrowserSteady { - return workerConfig{}, fmt.Errorf("client kind and lifecycle do not match") - } - if config.clientKind != rq2wire.ClientNative && config.clientKind != rq2wire.ClientBrowserWASM { - return workerConfig{}, fmt.Errorf("unsupported -client-kind %q", config.clientKind) - } - if config.requestTimeout <= 0 || config.requestTimeout > 24*time.Hour { - return workerConfig{}, fmt.Errorf("-request-timeout must be in (0,24h]") - } - return config, nil -} - -func canonicalLowerSHA256(value string) bool { - if len(value) != 64 { - return false - } - for _, char := range value { - if char < '0' || char > '9' { - if char < 'a' || char > 'f' { - return false - } - } - } - return true -} - -func readFixture(path string) ([]byte, error) { - lstat, err := os.Lstat(path) - if err != nil || lstat.Mode()&os.ModeSymlink != 0 || !lstat.Mode().IsRegular() { - return nil, fmt.Errorf("RQ2 fixture is not a regular non-symlink file: %v", err) - } - if lstat.Size() <= 0 || lstat.Size() > maxFixtureBytes { - return nil, fmt.Errorf("RQ2 fixture size must be in 1..%d bytes", maxFixtureBytes) - } - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() - opened, err := file.Stat() - if err != nil || !opened.Mode().IsRegular() || !os.SameFile(lstat, opened) { - return nil, fmt.Errorf("RQ2 fixture changed before it was opened") - } - data, err := io.ReadAll(io.LimitReader(file, maxFixtureBytes+1)) - if err != nil { - return nil, err - } - if int64(len(data)) != lstat.Size() { - return nil, fmt.Errorf("RQ2 fixture changed while being read") - } - post, err := os.Lstat(path) - if err != nil || !os.SameFile(lstat, post) || post.Mode()&os.ModeSymlink != 0 || post.Size() != lstat.Size() { - return nil, fmt.Errorf("RQ2 fixture changed after it was read") - } - return data, nil -} - -func decodeWorkerRequest(raw []byte) (rq2wire.WorkerRequest, error) { - if err := rejectDuplicateRequestKeys(raw); err != nil { - return rq2wire.WorkerRequest{}, fmt.Errorf("decode RQ2 request: %w", err) - } - decoder := json.NewDecoder(bytes.NewReader(raw)) - decoder.DisallowUnknownFields() - var request rq2wire.WorkerRequest - if err := decoder.Decode(&request); err != nil { - return rq2wire.WorkerRequest{}, fmt.Errorf("decode RQ2 request: %w", err) - } - if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { - return rq2wire.WorkerRequest{}, fmt.Errorf("RQ2 request must contain exactly one JSON object") - } - if err := request.Validate(); err != nil { - return rq2wire.WorkerRequest{}, err - } - return request, nil -} - -func rejectDuplicateRequestKeys(raw []byte) error { - decoder := json.NewDecoder(bytes.NewReader(raw)) - decoder.UseNumber() - if err := scanRequestJSONValue(decoder, "$"); err != nil { - return err - } - if _, err := decoder.Token(); !errors.Is(err, io.EOF) { - if err == nil { - return fmt.Errorf("JSON contains trailing data") - } - return err - } - return nil -} - -func scanRequestJSONValue(decoder *json.Decoder, path string) error { - token, err := decoder.Token() - if err != nil { - return err - } - delimiter, ok := token.(json.Delim) - if !ok { - return nil - } - switch delimiter { - case '{': - seen := map[string]struct{}{} - for decoder.More() { - keyToken, err := decoder.Token() - if err != nil { - return err - } - key, ok := keyToken.(string) - if !ok { - return fmt.Errorf("non-string object key at %s", path) - } - if _, exists := seen[key]; exists { - return fmt.Errorf("duplicate object key %q at %s", key, path) - } - seen[key] = struct{}{} - if err := scanRequestJSONValue(decoder, path+"."+key); err != nil { - return err - } - } - _, err := decoder.Token() - return err - case '[': - for index := 0; decoder.More(); index++ { - if err := scanRequestJSONValue(decoder, fmt.Sprintf("%s[%d]", path, index)); err != nil { - return err - } - } - _, err := decoder.Token() - return err - default: - return fmt.Errorf("unexpected JSON delimiter %q at %s", delimiter, path) - } -} - -func (w *worker) bindRequest(request rq2wire.WorkerRequest) error { - if request.WorkerID != w.config.workerID || request.PlatformID != w.config.platformID || request.ClientKind != w.config.clientKind || - request.Backend != w.config.backend || request.Lifecycle != w.config.lifecycle { - return fmt.Errorf("RQ2 request does not bind configured worker coordinate") - } - if w.sessionID == "" { - w.sessionID, w.fixtureID = request.SessionID, request.FixtureID - } - if request.SessionID != w.sessionID || request.FixtureID != w.fixtureID { - return fmt.Errorf("RQ2 request changed session or fixture identity") - } - if w.fixture == nil || request.FixtureID != w.fixture.FixtureID { - return fmt.Errorf("RQ2 request fixture identity does not match the pinned source fixture") - } - if request.RecordKind != w.state && !(w.state == rq2wire.RecordMutation && request.RecordKind == rq2wire.RecordSessionEnd) { - return fmt.Errorf("RQ2 request kind %q is out of order; want %q", request.RecordKind, w.state) - } - return nil -} - -func (w *worker) exchange(request rq2wire.WorkerRequest) rq2wire.WorkerRecord { - switch request.RecordKind { - case rq2wire.RecordPreflight: - return w.preflight(request) - case rq2wire.RecordSessionStart: - return w.startSession(request) - case rq2wire.RecordMutation: - return w.mutate(request) - case rq2wire.RecordSessionEnd: - return w.endSession(request) - default: - return rq2wire.FailedRecord(request, "input_invalid", fmt.Errorf("unsupported request kind")) - } -} - -func (w *worker) preflight(request rq2wire.WorkerRequest) rq2wire.WorkerRecord { - if w.config.clientKind == rq2wire.ClientBrowserWASM { - w.state = rq2wire.RecordSessionStart - return rq2wire.FailedRecord(request, "capability_unavailable", fmt.Errorf("real browser/WASM execution is not provided by the native worker; use a pinned browser worker")) - } - if w.config.lowPowerARM && runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" { - w.state = rq2wire.RecordSessionStart - return rq2wire.FailedRecord(request, "platform_mismatch", fmt.Errorf("low-power ARM registration cannot run on %s", runtime.GOARCH)) - } - native, err := newNativeSession(w.config, w.remote, w.evaluation, w.fixture) - if err != nil { - w.state = rq2wire.RecordSessionStart - return rq2wire.FailedRecord(request, "capability_unavailable", err) - } - w.native = native - record := rq2wire.BaseRecord(request) - record.Success = true - record.Capabilities = rq2wire.RequiredCapabilities(w.config.clientKind, w.config.backend) - record.Runtime = &rq2wire.RuntimeEvidence{ - OS: runtime.GOOS, Architecture: runtime.GOARCH, LowPowerARM: w.config.lowPowerARM, - MachineDescriptorID: w.config.machine.Descriptor.ID, MachineDescriptorSHA256: w.config.machine.SHA256, - } - w.state = rq2wire.RecordSessionStart - return record -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-worker/main_test.go b/tools/evaluation/cmd/malt-eval-rq2-worker/main_test.go deleted file mode 100644 index a6d08ff..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-worker/main_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package main - -import ( - "bytes" - "io" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "runtime" - "strings" - "testing" -) - -func TestParseFlagsRequiresExplicitLowPowerClassification(t *testing.T) { - base := []string{ - "-gateway-base-url", "http://127.0.0.1:8080", "-fixture", "/tmp/fixture", - "-worker-id", "worker", "-platform-id", "host", "-client-kind", "native", - "-backend", "kzg", "-lifecycle", "native-long-lived", - "-gateway-instance-token", strings.Repeat("a", 64), - "-machine-descriptor", "/tmp/machine.json", "-machine-descriptor-sha256", strings.Repeat("c", 64), "-machine-descriptor-bytes", "1", - } - if _, err := parseFlags(base, &bytes.Buffer{}); err == nil || !strings.Contains(err.Error(), "low-power-arm") { - t.Fatalf("missing low-power classification error = %v", err) - } - config, err := parseFlags(append(base, "-low-power-arm=false"), &bytes.Buffer{}) - if err != nil || config.lowPowerARM { - t.Fatalf("explicit host classification = %#v, %v", config, err) - } -} - -func TestRunFailsClosedOnGatewayInstanceTokenMismatch(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - if request.URL.Path != "/healthz" { - http.NotFound(response, request) - return - } - _, _ = io.WriteString(response, `{"status":"ok","evaluation_instance_token":"`+strings.Repeat("b", 64)+`"}`) - })) - defer server.Close() - fixture := filepath.Join(t.TempDir(), "fixture.bin") - if err := os.WriteFile(fixture, []byte("fixture"), 0o600); err != nil { - t.Fatal(err) - } - args := []string{ - "-gateway-base-url", server.URL, "-fixture", fixture, "-worker-id", "worker", "-platform-id", "host", - "-client-kind", "native", "-backend", "kzg", "-lifecycle", "native-long-lived", - "-low-power-arm=false", "-gateway-instance-token", strings.Repeat("a", 64), "-request-timeout", "5s", - "-machine-descriptor", "/tmp/machine.json", "-machine-descriptor-sha256", strings.Repeat("c", 64), "-machine-descriptor-bytes", "1", - } - err := run(args, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{}) - if err == nil || !strings.Contains(err.Error(), "exact registered disposable instance token") { - t.Fatalf("token mismatch error = %v", err) - } -} - -func TestReadFixtureRejectsSymlinkAndDetectsContent(t *testing.T) { - directory := t.TempDir() - target := filepath.Join(directory, "fixture.bin") - if err := os.WriteFile(target, []byte("fixture"), 0o600); err != nil { - t.Fatal(err) - } - data, err := readFixture(target) - if err != nil || string(data) != "fixture" { - t.Fatalf("read fixture = %q, %v", data, err) - } - link := filepath.Join(directory, "fixture-link") - if err := os.Symlink(target, link); err != nil { - if runtime.GOOS == "windows" { - t.Skipf("symlink unavailable: %v", err) - } - t.Fatal(err) - } - if _, err := readFixture(link); err == nil { - t.Fatal("fixture symlink was accepted") - } -} - -func TestDecodeWorkerRequestRejectsUnknownAndTrailingJSON(t *testing.T) { - valid := `{"schema_version":"malt-rq2-worker-request/v1","worker_id":"worker","request_id":"preflight","record_kind":"preflight","session_id":"session","client_kind":"native","platform_id":"host","backend":"kzg","lifecycle":"native-long-lived","fixture_id":"fixture","measured":false}` - if _, err := decodeWorkerRequest([]byte(valid)); err != nil { - t.Fatal(err) - } - for _, hostile := range []string{ - strings.TrimSuffix(valid, "}") + `,"unknown":true}`, - strings.Replace(valid, `"worker_id":"worker"`, `"worker_id":"worker","worker_id":"other"`, 1), - valid + `{}`, - } { - if _, err := decodeWorkerRequest([]byte(hostile)); err == nil { - t.Fatalf("hostile request was accepted: %s", hostile) - } - } -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-worker/native.go b/tools/evaluation/cmd/malt-eval-rq2-worker/native.go deleted file mode 100644 index fb78985..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-worker/native.go +++ /dev/null @@ -1,310 +0,0 @@ -package main - -import ( - "context" - "encoding/hex" - "errors" - "fmt" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/authenticationgraph" - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2metrics" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2wire" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2write" - "github.com/dewebprotocol/malt-client/transport" - "github.com/dewebprotocol/malt-core/auth/commitment/ipa" - "github.com/dewebprotocol/malt-core/auth/commitment/kzg" - "github.com/dewebprotocol/malt-core/engine" - cid "github.com/ipfs/go-cid" -) - -var nativeOperations = map[string]struct{}{ - "append": {}, "batch-sync": {}, "create-small-file": {}, "delete-directory-entry": {}, - "insert-directory-entry": {}, "modify-small-file": {}, "move": {}, "rename": {}, - "replace-large-file-chunk": {}, -} - -type nativeSession struct { - config workerConfig - remote *transport.Client - app *authenticationgraph.Session - fixture *rq2fixture.Fixture - workspace *nativeWorkspace - root cid.Cid - serial uint64 -} - -func newNativeSession(config workerConfig, remote *transport.Client, evaluation *gatewaytransport.Client, fixture *rq2fixture.Fixture) (*nativeSession, error) { - if config.clientKind != rq2wire.ClientNative || config.lifecycle != rq2wire.LifecycleNativeLong { - return nil, fmt.Errorf("native worker requires native-long-lived coordinate") - } - var scheme engine.Profile - var err error - switch config.backend { - case "kzg": - scheme, err = kzg.NewScheme() - case "ipa": - scheme, err = ipa.NewScheme() - default: - return nil, fmt.Errorf("unsupported backend %q", config.backend) - } - if err != nil { - return nil, err - } - registry := engine.NewRegistry() - if err := registry.Register(scheme); err != nil { - return nil, err - } - app, err := authenticationgraph.New(evaluation, engine.New(registry)) - if err != nil { - return nil, err - } - if fixture == nil { - return nil, fmt.Errorf("native RQ2 source fixture is nil") - } - if _, err := fixture.Root(config.backend); err != nil { - return nil, err - } - operations := make([]string, 0, len(nativeOperations)) - for operation := range nativeOperations { - operations = append(operations, operation) - } - if err := fixture.RequireOperations(operations); err != nil { - return nil, err - } - return &nativeSession{config: config, remote: remote, app: app, fixture: fixture}, nil -} - -func (n *nativeSession) close() error { - if n == nil { - return nil - } - return n.workspace.close() -} - -func (w *worker) startSession(request rq2wire.WorkerRequest) rq2wire.WorkerRecord { - w.state = rq2wire.RecordMutation - if w.native == nil { - return rq2wire.FailedRecord(request, "capability_unavailable", fmt.Errorf("native capability did not pass preflight")) - } - root, err := cid.Parse(request.ExpectedAcceptedRoot) - if err != nil || !rq2wire.ValidTypedRoot(root.String(), w.config.backend) { - return rq2wire.FailedRecord(request, "input_invalid", fmt.Errorf("session root is not a typed %s MALT root", w.config.backend)) - } - ctx, cancel := context.WithTimeout(context.Background(), w.config.requestTimeout) - defer cancel() - load, err := w.native.app.Load(ctx, root) - if err != nil { - return rq2wire.FailedRecord(request, classifyNativeFailure(err), fmt.Errorf("load initial authentication graph: %w", err)) - } - view, err := w.native.app.Snapshot() - if err != nil { - return rq2wire.FailedRecord(request, "verification_failed", err) - } - if err := w.native.fixture.ValidateInitialGraph(view, w.config.backend); err != nil { - return rq2wire.FailedRecord(request, "fixture_incompatible", err) - } - workspace, err := newNativeWorkspace(w.native.fixture) - if err != nil { - return rq2wire.FailedRecord(request, "fixture_incompatible", fmt.Errorf("create native UnixFS workspace: %w", err)) - } - if w.native.workspace != nil { - _ = w.native.workspace.close() - } - w.native.workspace = workspace - w.native.root = root - record := rq2wire.BaseRecord(request) - record.Success = true - record.Session = &rq2wire.SessionEvidence{AcceptedRoot: root.String(), GraphFetch: rq2wire.ObservedPhase(load.FetchNS, load.WireBytes, load.Objects), GraphImport: rq2wire.ObservedPhase(load.ImportNS, load.WireBytes, load.Objects)} - return record -} - -func (w *worker) mutate(request rq2wire.WorkerRequest) rq2wire.WorkerRecord { - if _, ok := nativeOperations[request.Operation]; !ok { - return rq2wire.FailedRecord(request, "operation_unsupported", fmt.Errorf("unsupported native operation %q", request.Operation)) - } - if w.native == nil || !w.native.root.Defined() || request.ExpectedAcceptedRoot != w.native.root.String() { - return rq2wire.FailedRecord(request, "root_continuity", fmt.Errorf("mutation expected root does not match retained session root")) - } - ctx, cancel := context.WithTimeout(context.Background(), w.config.requestTimeout) - defer cancel() - usageBefore, err := readProcessUsage() - if err != nil { - return rq2wire.FailedRecord(request, "measurement_unavailable", err) - } - mutationStarted := time.Now() - viewSnapshotStarted := time.Now() - view, err := w.native.app.Snapshot() - viewSnapshotNS := durationNS(time.Since(viewSnapshotStarted)) - if err != nil { - return rq2wire.FailedRecord(request, "verification_failed", err) - } - currentSource, err := w.native.workspace.snapshot() - if err != nil { - return rq2wire.FailedRecord(request, "fixture_incompatible", fmt.Errorf("scan native source pre-image: %w", err)) - } - if err := w.native.fixture.ValidateGraphAgainstSource(view, w.config.backend, currentSource); err != nil { - return rq2wire.FailedRecord(request, "fixture_incompatible", fmt.Errorf("native source pre-image binding: %w", err)) - } - operation, err := w.native.fixture.Operation(request.Operation) - if err != nil { - return rq2wire.FailedRecord(request, "fixture_incompatible", err) - } - prepared, err := prepareNativeOperation(operation, w.native.fixture, w.native.workspace, w.native.serial) - if err != nil { - return rq2wire.FailedRecord(request, "fixture_incompatible", err) - } - var payloadUpload transport.PutBatchMeasurement - if len(prepared.blocks) > 0 { - payloadUpload, err = w.native.remote.PutBatchMeasured(ctx, prepared.blocks) - if err != nil { - return rq2wire.FailedRecord(request, classifyNativeFailure(err), fmt.Errorf("upload operation payloads: %w", err)) - } - results := payloadUpload.Results - if len(results) != len(prepared.cids) { - return rq2wire.FailedRecord(request, "transport_invalid", fmt.Errorf("CAS batch returned a mismatched result count")) - } - for index := range results { - if !results[index].CID.Equals(prepared.cids[index]) { - return rq2wire.FailedRecord(request, "transport_invalid", fmt.Errorf("CAS result %d does not match locally hashed payload", index)) - } - } - } - projectionStarted := time.Now() - edit, err := w.native.app.Begin() - if err != nil { - return rq2wire.FailedRecord(request, "verification_failed", err) - } - candidate, err := rq2write.Apply(ctx, edit, w.native.root, operation, prepared.cids) - projectionNS := durationNS(time.Since(projectionStarted)) - if err != nil { - return rq2wire.FailedRecord(request, "fixture_incompatible", err) - } - operationID := exactOperationID(request) - result, err := w.native.app.Submit(ctx, operationID, edit, candidate) - if err != nil { - return rq2wire.FailedRecord(request, classifyNativeFailure(err), err) - } - if result.Idempotent { - return rq2wire.FailedRecord(request, "gateway_instance_reused", fmt.Errorf("disposable Gateway returned an idempotent authentication batch replay")) - } - mutationTotalNS := durationNS(time.Since(mutationStarted)) - usageAfter, err := readProcessUsage() - if err != nil || usageAfter.peakRSSBytes == 0 { - return rq2wire.FailedRecord(request, "measurement_unavailable", fmt.Errorf("read process usage after mutation: %w", err)) - } - cpuNS := usageAfter.cpuNS - min(usageAfter.cpuNS, usageBefore.cpuNS) - postView, err := w.native.app.Snapshot() - if err != nil { - return rq2wire.FailedRecord(request, "verification_failed", fmt.Errorf("snapshot native post-image: %w", err)) - } - postSource, err := w.native.workspace.snapshot() - if err != nil { - return rq2wire.FailedRecord(request, "fixture_incompatible", fmt.Errorf("scan native source post-image: %w", err)) - } - if err := w.native.fixture.ValidateGraphAgainstSource(postView, w.config.backend, postSource); err != nil { - return rq2wire.FailedRecord(request, "verification_failed", fmt.Errorf("native full source post-image oracle: %w", err)) - } - encodedCandidate, err := encodeExpectedRoot(result.Root) - if err != nil { - return rq2wire.FailedRecord(request, "verification_failed", err) - } - record := rq2wire.BaseRecord(request) - record.Success = true - metrics, err := nativeMetrics(prepared, result, viewSnapshotNS, projectionNS, payloadUpload, mutationTotalNS, cpuNS, usageAfter.peakRSSBytes) - if err != nil { - return rq2wire.FailedRecord(request, "measurement_invalid", err) - } - if err := metrics.Validate(w.config.clientKind, w.config.backend, w.config.lifecycle, request.Operation); err != nil { - return rq2wire.FailedRecord(request, "measurement_invalid", err) - } - record.Mutation = &rq2wire.MutationEvidence{ - Operation: request.Operation, PriorRoot: result.Base.String(), CandidateRoot: encodedCandidate, - ReceiptRoot: result.Receipt.Root, ReceiptAccepted: result.Receipt.Root == result.Root.String(), - BatchSHA256: result.Receipt.Digest, - Metrics: metrics, - } - w.native.root = result.Root - w.native.serial++ - w.receiptCount++ - return record -} - -func (w *worker) endSession(request rq2wire.WorkerRequest) rq2wire.WorkerRecord { - w.state = "finished" - if w.native == nil || !w.native.root.Defined() || request.ExpectedAcceptedRoot != w.native.root.String() { - return rq2wire.FailedRecord(request, "root_continuity", fmt.Errorf("session-end root does not match retained exact receipt root")) - } - ctx, cancel := context.WithTimeout(context.Background(), w.config.requestTimeout) - defer cancel() - if err := w.native.app.Audit(ctx); err != nil { - return rq2wire.FailedRecord(request, "audit_failed", err) - } - record := rq2wire.BaseRecord(request) - record.Success = true - record.Session = &rq2wire.SessionEvidence{AcceptedRoot: w.native.root.String(), ReceiptCount: w.receiptCount, AuditPassed: true, GraphFetch: rq2wire.NotApplicablePhase(), GraphImport: rq2wire.NotApplicablePhase()} - return record -} - -func nativeMetrics(prepared preparedPayloads, operation authenticationgraph.Result, snapshotNS, projectionNS uint64, payloadUpload transport.PutBatchMeasurement, mutationTotalNS, cpuNS, peakRSS uint64) (rq2wire.MutationMetrics, error) { - phase := func(duration, bytes, count uint64) rq2wire.PhaseMeasurement { - return rq2wire.ObservedPhase(duration, bytes, max(uint64(1), count)) - } - upload := rq2wire.NotApplicablePhase() - if len(prepared.blocks) > 0 { - upload = phase(payloadUpload.RoundTripNS, payloadUpload.RequestWireBytes, uint64(len(prepared.blocks))) - } - generation, err := rq2metrics.AddDurations(snapshotNS, projectionNS) - if err != nil { - return rq2wire.MutationMetrics{}, err - } - computation, submission := operation.Computation, operation.Submission - return rq2wire.MutationMetrics{ - TaxonomyProfile: rq2metrics.TaxonomyProfile, MutationTotal: phase(mutationTotalNS, 0, 1), Scan: prepared.scan, Chunk: prepared.chunk, Hash: prepared.hash, - GraphSnapshot: phase(snapshotNS, 0, 1), CandidateApply: phase(computation.ApplyNS, 0, computation.Candidates), CandidateExport: phase(computation.ExportNS, computation.CandidateBytes, computation.Candidates), CandidateGeneration: phase(generation, 0, 1), - BatchEncoding: phase(submission.RequestEncodingNS, submission.RequestWireBytes, 1), Upload: upload, - GatewayValidateStage: phase(submission.Gateway.ValidationAndStageNS, 0, 1), GatewayPersist: phase(submission.Gateway.PersistNS, 0, 1), ReceiptCheck: phase(submission.ResponseVerifyNS, submission.ResponseWireBytes, 1), - CPUTotal: phase(cpuNS, 0, 1), PeakMemory: phase(0, peakRSS, 1), WASMDownload: rq2wire.NotApplicablePhase(), WASMInstantiate: rq2wire.NotApplicablePhase(), ParameterLoad: rq2wire.NotApplicablePhase(), FirstMutation: rq2wire.NotApplicablePhase(), JSWASMBoundary: rq2wire.NotApplicablePhase(), - }, nil -} - -func encodeExpectedRoot(candidate cid.Cid) (string, error) { - encoded := candidate.String() - parsed, err := cid.Parse(encoded) - if err != nil || !parsed.Equals(candidate) { - return "", fmt.Errorf("expected-root CID encoding did not round-trip") - } - return encoded, nil -} - -func exactOperationID(request rq2wire.WorkerRequest) string { - value := request.WorkerID + "\x00" + request.SessionID + "\x00" + request.RequestID + "\x00" + request.Operation - digest := mutationDigest([]byte(value)) - return "rq2-" + hex.EncodeToString(digest[:16]) -} - -func classifyNativeFailure(err error) string { - if err == nil { - return "unknown" - } - if _, ok := err.(*transport.Error); ok { - return "gateway_rejected" - } - if contextCanceled(err) { - return "timeout" - } - return "authentication_failed" -} - -func contextCanceled(err error) bool { - return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) -} - -func durationNS(value time.Duration) uint64 { - if value <= 0 { - return 0 - } - return uint64(value) -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-worker/planner.go b/tools/evaluation/cmd/malt-eval-rq2-worker/planner.go deleted file mode 100644 index 4dc1db2..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-worker/planner.go +++ /dev/null @@ -1,312 +0,0 @@ -package main - -import ( - "crypto/sha256" - "fmt" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2wire" - "io/fs" - "os" - "path/filepath" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture" - "github.com/dewebprotocol/malt-client/transport" - cid "github.com/ipfs/go-cid" - mh "github.com/multiformats/go-multihash" -) - -type preparedPayloads struct { - blocks []transport.Block - cids []cid.Cid - scan rq2wire.PhaseMeasurement - chunk rq2wire.PhaseMeasurement - hash rq2wire.PhaseMeasurement -} - -type nativeWorkspace struct { - root string -} - -type changedFileRange struct { - path string - offset int64 - length int -} - -func newNativeWorkspace(fixture *rq2fixture.Fixture) (*nativeWorkspace, error) { - if fixture == nil { - return nil, fmt.Errorf("RQ2 source fixture is nil") - } - root, err := os.MkdirTemp("", "malt-rq2-native-workspace-") - if err != nil { - return nil, err - } - workspace := &nativeWorkspace{root: root} - failed := true - defer func() { - if failed { - _ = workspace.close() - } - }() - for relative, data := range fixture.InitialSource() { - path := filepath.Join(root, filepath.FromSlash(relative)) - if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { - return nil, err - } - if err := os.WriteFile(path, data, 0o600); err != nil { - return nil, err - } - } - failed = false - return workspace, nil -} - -func (w *nativeWorkspace) snapshot() (map[string][]byte, error) { - if w == nil || w.root == "" { - return nil, fmt.Errorf("native filesystem workspace is unavailable") - } - source := make(map[string][]byte) - var total uint64 - err := filepath.WalkDir(w.root, func(path string, entry fs.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - if path == w.root || entry.IsDir() { - return nil - } - info, err := entry.Info() - if err != nil { - return err - } - if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() { - return fmt.Errorf("native source contains a non-regular file %q", path) - } - data, err := os.ReadFile(path) - if err != nil { - return err - } - total += uint64(len(data)) - if total > maxFixtureBytes { - return fmt.Errorf("native source post-image exceeds %d bytes", maxFixtureBytes) - } - relative, err := filepath.Rel(w.root, path) - if err != nil { - return err - } - relative = filepath.ToSlash(relative) - if _, exists := source[relative]; exists { - return fmt.Errorf("native source repeats path %q", relative) - } - source[relative] = data - return nil - }) - if err != nil { - return nil, err - } - return source, nil -} - -func (w *nativeWorkspace) close() error { - if w == nil || w.root == "" { - return nil - } - root := w.root - w.root = "" - return os.RemoveAll(root) -} - -// prepareNativeOperation mutates a real temporary filesystem tree and reads -// only the file bytes changed by that operation. Metadata-only operations -// execute a real filesystem rename/remove and explicitly report the payload -// scan/chunk/hash stages as not applicable. -func prepareNativeOperation(operation rq2fixture.Operation, fixture *rq2fixture.Fixture, workspace *nativeWorkspace, serial uint64) (preparedPayloads, error) { - if workspace == nil || workspace.root == "" { - return preparedPayloads{}, fmt.Errorf("native filesystem workspace is unavailable") - } - count, sizes, err := payloadShape(operation) - if err != nil { - return preparedPayloads{}, err - } - changed, err := workspace.apply(operation, fixture, serial, sizes) - if err != nil { - return preparedPayloads{}, fmt.Errorf("apply native filesystem operation %s: %w", operation.Name, err) - } - if count == 0 { - if len(changed) != 0 { - return preparedPayloads{}, fmt.Errorf("metadata-only operation unexpectedly changed payload bytes") - } - return preparedPayloads{scan: rq2wire.NotApplicablePhase(), chunk: rq2wire.NotApplicablePhase(), hash: rq2wire.NotApplicablePhase()}, nil - } - if len(changed) != count { - return preparedPayloads{}, fmt.Errorf("filesystem operation changed %d payloads, want %d", len(changed), count) - } - scanStarted := time.Now() - scanned := make([][]byte, len(changed)) - var changedBytes uint64 - for index, item := range changed { - file, err := os.Open(item.path) - if err != nil { - return preparedPayloads{}, err - } - data := make([]byte, item.length) - _, readErr := file.ReadAt(data, item.offset) - closeErr := file.Close() - if readErr != nil { - return preparedPayloads{}, readErr - } - if closeErr != nil { - return preparedPayloads{}, closeErr - } - scanned[index] = data - changedBytes += uint64(len(data)) - } - prepared := preparedPayloads{scan: rq2wire.ObservedPhase(durationNS(time.Since(scanStarted)), changedBytes, uint64(len(scanned)))} - chunkStarted := time.Now() - chunks := make([][]byte, len(scanned)) - for index := range scanned { - chunks[index] = append([]byte(nil), scanned[index]...) - } - prepared.chunk = rq2wire.ObservedPhase(durationNS(time.Since(chunkStarted)), changedBytes, uint64(len(chunks))) - hashStarted := time.Now() - prepared.cids = make([]cid.Cid, len(chunks)) - prepared.blocks = make([]transport.Block, len(chunks)) - for index, chunk := range chunks { - digest, err := mh.Sum(chunk, mh.SHA2_256, -1) - if err != nil { - return preparedPayloads{}, err - } - prepared.cids[index] = cid.NewCidV1(cid.Raw, digest) - prepared.blocks[index] = transport.Block{Codec: cid.Raw, Data: chunk} - } - prepared.hash = rq2wire.ObservedPhase(durationNS(time.Since(hashStarted)), changedBytes, uint64(len(chunks))) - return prepared, nil -} - -func (w *nativeWorkspace) apply(operation rq2fixture.Operation, fixture *rq2fixture.Fixture, serial uint64, sizes []int) ([]changedFileRange, error) { - payload := func(index int) ([]byte, error) { - return fixture.Payload(operation.Name, serial, index, uint64(sizes[index])) - } - write := func(relative string, data []byte) (changedFileRange, error) { - path := filepath.Join(w.root, filepath.FromSlash(relative)) - if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { - return changedFileRange{}, err - } - if err := os.WriteFile(path, data, 0o600); err != nil { - return changedFileRange{}, err - } - return changedFileRange{path: path, length: len(data)}, nil - } - switch operation.Kind { - case rq2fixture.KindListAppend: - path := filepath.Join(w.root, filepath.FromSlash(operation.SourcePath)) - file, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0) - if err != nil { - return nil, err - } - stat, err := file.Stat() - data, payloadErr := payload(0) - if err == nil && payloadErr == nil { - _, err = file.Write(data) - } - closeErr := file.Close() - if payloadErr != nil { - return nil, payloadErr - } - if err != nil { - return nil, err - } - if closeErr != nil { - return nil, closeErr - } - return []changedFileRange{{path: path, offset: stat.Size(), length: sizes[0]}}, nil - case rq2fixture.KindListReplace: - if operation.ListIndex == nil { - return nil, fmt.Errorf("list replacement omits its index") - } - list, ok := fixture.List(operation.SourcePath) - if !ok { - return nil, fmt.Errorf("list replacement source is not declared") - } - path := filepath.Join(w.root, filepath.FromSlash(operation.SourcePath)) - file, err := os.OpenFile(path, os.O_WRONLY, 0) - if err != nil { - return nil, err - } - data, payloadErr := payload(0) - if payloadErr == nil { - _, err = file.WriteAt(data, int64(*operation.ListIndex*list.ChunkSize)) - } - closeErr := file.Close() - if payloadErr != nil { - return nil, payloadErr - } - if err != nil { - return nil, err - } - if closeErr != nil { - return nil, closeErr - } - return []changedFileRange{{path: path, offset: int64(*operation.ListIndex * list.ChunkSize), length: sizes[0]}}, nil - case rq2fixture.KindDirectReplace: - data, err := payload(0) - if err != nil { - return nil, err - } - item, err := write(operation.SourcePath, data) - return []changedFileRange{item}, err - case rq2fixture.KindDirectInsert: - if _, err := os.Stat(filepath.Join(w.root, filepath.FromSlash(operation.DestinationPath))); !os.IsNotExist(err) { - return nil, fmt.Errorf("destination %q is not absent", operation.DestinationPath) - } - data, err := payload(0) - if err != nil { - return nil, err - } - item, err := write(operation.DestinationPath, data) - return []changedFileRange{item}, err - case rq2fixture.KindBatchInsert: - changed := make([]changedFileRange, len(sizes)) - for index := range sizes { - if _, err := os.Stat(filepath.Join(w.root, filepath.FromSlash(operation.Batch[index].Path))); !os.IsNotExist(err) { - return nil, fmt.Errorf("batch destination %q is not absent", operation.Batch[index].Path) - } - data, err := payload(index) - if err != nil { - return nil, err - } - item, err := write(operation.Batch[index].Path, data) - if err != nil { - return nil, err - } - changed[index] = item - } - return changed, nil - case rq2fixture.KindDirectDelete: - return nil, os.Remove(filepath.Join(w.root, filepath.FromSlash(operation.SourcePath))) - case rq2fixture.KindDirectMove: - destination := filepath.Join(w.root, filepath.FromSlash(operation.DestinationPath)) - if err := os.MkdirAll(filepath.Dir(destination), 0o700); err != nil { - return nil, err - } - return nil, os.Rename(filepath.Join(w.root, filepath.FromSlash(operation.SourcePath)), destination) - default: - return nil, fmt.Errorf("unsupported native filesystem operation kind %q", operation.Kind) - } -} - -func payloadShape(operation rq2fixture.Operation) (int, []int, error) { - switch operation.Kind { - case rq2fixture.KindDirectDelete, rq2fixture.KindDirectMove: - return 0, nil, nil - case rq2fixture.KindBatchInsert: - sizes := make([]int, len(operation.Batch)) - for index, target := range operation.Batch { - sizes[index] = int(target.PayloadBytes) - } - return len(sizes), sizes, nil - default: - return 1, []int{int(operation.PayloadBytes)}, nil - } -} - -func mutationDigest(value []byte) [32]byte { return sha256.Sum256(value) } diff --git a/tools/evaluation/cmd/malt-eval-rq2-worker/planner_test.go b/tools/evaluation/cmd/malt-eval-rq2-worker/planner_test.go deleted file mode 100644 index 2efd668..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-worker/planner_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strings" - "testing" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2e0" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2wire" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2write" - "github.com/dewebprotocol/malt-client/transport" -) - -func TestNativePlannerExecutesAllRegisteredOperationsWithKZGAndIPA(t *testing.T) { - raw, err := os.ReadFile(writeNativeE0Fixture(t)) - if err != nil { - t.Fatal(err) - } - fixture, err := rq2fixture.Decode(raw) - if err != nil { - t.Fatal(err) - } - for _, backend := range []string{"kzg", "ipa"} { - t.Run(backend, func(t *testing.T) { - gateway, root, err := rq2e0.NewConformanceGateway(fixture, backend, strings.Repeat("b", 64)) - if err != nil { - t.Fatal(err) - } - defer gateway.Close() - evaluation, err := gatewaytransport.New(gatewaytransport.Options{BaseURL: gateway.URL(), InstanceToken: strings.Repeat("b", 64)}) - if err != nil { - t.Fatal(err) - } - remote, err := transport.New(transport.Options{BaseURL: gateway.URL(), HTTPClient: evaluation.InstanceHTTPClient()}) - if err != nil { - t.Fatal(err) - } - native, err := newNativeSession(workerConfig{clientKind: rq2wire.ClientNative, lifecycle: rq2wire.LifecycleNativeLong, backend: backend, requestTimeout: 30 * time.Second}, remote, evaluation, fixture) - if err != nil { - t.Fatal(err) - } - if _, err := native.app.Load(t.Context(), root); err != nil { - t.Fatal(err) - } - workspace, err := newNativeWorkspace(fixture) - if err != nil { - t.Fatal(err) - } - defer workspace.close() - for i, name := range []string{"append", "batch-sync", "create-small-file", "delete-directory-entry", "insert-directory-entry", "modify-small-file", "move", "rename", "replace-large-file-chunk"} { - operation, err := fixture.Operation(name) - if err != nil { - t.Fatal(err) - } - prepared, err := prepareNativeOperation(operation, fixture, workspace, uint64(i)) - if err != nil { - t.Fatal(err) - } - metadata := name == "delete-directory-entry" || name == "move" || name == "rename" - if metadata { - if prepared.scan.Applicable || prepared.chunk.Applicable || prepared.hash.Applicable || len(prepared.blocks) > 0 { - t.Fatalf("metadata-only operation %s claimed payload work", name) - } - } else { - if !prepared.scan.Applicable || !prepared.chunk.Applicable || !prepared.hash.Applicable || prepared.scan.Bytes == 0 || prepared.chunk.Count == 0 || prepared.hash.Count == 0 { - t.Fatalf("payload phases missing for %s", name) - } - uploaded, err := remote.PutBatchMeasured(t.Context(), prepared.blocks) - if err != nil { - t.Fatal(err) - } - if len(uploaded.Results) != len(prepared.cids) { - t.Fatal("payload result count differs") - } - for j, result := range uploaded.Results { - if !result.CID.Equals(prepared.cids[j]) { - t.Fatal("payload digest differs") - } - } - } - edit, err := native.app.Begin() - if err != nil { - t.Fatal(err) - } - next, err := rq2write.Apply(t.Context(), edit, root, operation, prepared.cids) - if err != nil { - t.Fatalf("%s: %v", name, err) - } - view, err := edit.Preview(t.Context(), next) - if err != nil { - t.Fatal(err) - } - source, err := workspace.snapshot() - if err != nil { - t.Fatal(err) - } - if err := fixture.ValidateGraphAgainstSource(view, backend, source); err != nil { - t.Fatalf("%s full filesystem post-image: %v", name, err) - } - result, err := native.app.Submit(t.Context(), fmt.Sprintf("native-%d", i), edit, next) - if err != nil { - t.Fatal(err) - } - root = result.Root - } - if err := native.app.Audit(t.Context()); err != nil { - t.Fatal(err) - } - if _, err := os.Stat(workspace.root); err != nil { - t.Fatal(err) - } - }) - } -} -func TestBrowserPreflightFailsClosedWithoutRealBrowserWorker(t *testing.T) { - w := &worker{config: workerConfig{clientKind: rq2wire.ClientBrowserWASM, backend: "kzg", lifecycle: rq2wire.LifecycleBrowserCold}} - request := rq2wire.WorkerRequest{SchemaVersion: rq2wire.WorkerRequestSchema, WorkerID: "worker", RequestID: "preflight", RecordKind: rq2wire.RecordPreflight, SessionID: "session", ClientKind: rq2wire.ClientBrowserWASM, PlatformID: "browser", Backend: "kzg", Lifecycle: rq2wire.LifecycleBrowserCold, FixtureID: "fixture"} - record := w.preflight(request) - if record.Success || record.FailureClass != "capability_unavailable" { - t.Fatalf("native worker claimed browser execution: %+v", record) - } -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-worker/rss_bytes.go b/tools/evaluation/cmd/malt-eval-rq2-worker/rss_bytes.go deleted file mode 100644 index 84463f5..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-worker/rss_bytes.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build darwin - -package main - -const unixRSSIsKiB = false diff --git a/tools/evaluation/cmd/malt-eval-rq2-worker/rss_kib.go b/tools/evaluation/cmd/malt-eval-rq2-worker/rss_kib.go deleted file mode 100644 index b781503..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-worker/rss_kib.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build aix || dragonfly || freebsd || linux || netbsd || openbsd || solaris - -package main - -const unixRSSIsKiB = true diff --git a/tools/evaluation/cmd/malt-eval-rq2-worker/selftest.go b/tools/evaluation/cmd/malt-eval-rq2-worker/selftest.go deleted file mode 100644 index 3ad4a97..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-worker/selftest.go +++ /dev/null @@ -1,501 +0,0 @@ -package main - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "os" - "path/filepath" - "slices" - "strconv" - "strings" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/e0selftest" - "github.com/dewebprotocol/malt-client/internal/evaluation/machine" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2e0" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2wire" -) - -const ( - nativeE0CapabilityID = "rq2.native-writer" - nativeE0ProfileID = "rq2-native-writer-positive-hostile-v2" - // Updated only when the ordered compiled case contract intentionally - // changes together with the evaluator-owned registry. - nativeE0ExpectedProfileSHA256 = "189e22ffdc2aeee2a044ff8df0d31a9dc2482170a0d61a468fdca320761b3014" -) - -var nativeE0Profile = e0selftest.Profile{ - ProfileID: nativeE0ProfileID, - PositiveCases: []string{ - "execute-native-kzg-operation-matrix", - "execute-native-ipa-operation-matrix", - }, - HostileCases: []string{ - "reject-malformed-request", - "reject-backend-root-substitution", - "reject-stale-session-root", - "reject-tampered-fixture", - "reject-machine-descriptor-pin-substitution", - }, -} - -var nativeE0Operations = []string{ - "append", "batch-sync", "create-small-file", "delete-directory-entry", "insert-directory-entry", - "modify-small-file", "move", "rename", "replace-large-file-chunk", -} - -type nativeE0Config struct { - fixturePath string - machinePath string - timeout time.Duration - fixture *rq2fixture.Fixture - machine *machine.Evidence - machinePin rq2e0.FilePin -} - -type nativeE0Execution struct { - gateway *rq2e0.ConformanceGateway - harness *rq2e0.Harness - root string -} - -func (e *nativeE0Execution) abort() { - if e == nil { - return - } - if e.harness != nil { - e.harness.Abort() - } - if e.gateway != nil { - e.gateway.Close() - } -} - -func runNativeE0SelfTest(arguments []string, output, stderr io.Writer) error { - config, err := parseNativeE0Flags(arguments, stderr) - if err != nil { - return err - } - config, err = prepareNativeE0Inputs(config) - if err != nil { - return err - } - contract, err := nativeE0Profile.Contract() - if err != nil { - return err - } - if contract.ProfileSHA256 != nativeE0ExpectedProfileSHA256 { - return fmt.Errorf("compiled native-writer E0 profile digest drifted: got %s", contract.ProfileSHA256) - } - results := make([]e0selftest.CaseResult, 0, len(nativeE0Profile.PositiveCases)+len(nativeE0Profile.HostileCases)) - for _, testCase := range nativeE0Profile.PositiveCases { - backend := "kzg" - if strings.Contains(testCase, "ipa") { - backend = "ipa" - } - if err := executeNativeE0Matrix(config, backend, testCase); err != nil { - return fmt.Errorf("native E0 positive case %q: %w", testCase, err) - } - results = append(results, e0selftest.CaseResult{ID: testCase, Passed: true}) - } - for _, testCase := range nativeE0Profile.HostileCases { - if err := executeNativeE0Hostile(config, testCase); err != nil { - return fmt.Errorf("native E0 hostile case %q: %w", testCase, err) - } - results = append(results, e0selftest.CaseResult{ID: testCase, Passed: true}) - } - receipt, err := e0selftest.Issue(nativeE0CapabilityID, nativeE0Profile, results) - if err != nil { - return err - } - encoder := json.NewEncoder(output) - encoder.SetEscapeHTML(false) - return encoder.Encode(receipt) -} - -func parseNativeE0Flags(arguments []string, stderr io.Writer) (nativeE0Config, error) { - flags := flag.NewFlagSet("malt-eval-rq2-worker --e0-self-test", flag.ContinueOnError) - flags.SetOutput(stderr) - fixture := flags.String("fixture", "", "pinned RQ2 source fixture corpus") - machinePath := flags.String("machine-descriptor", "", "pinned platform machine descriptor") - timeout := flags.Duration("request-timeout", 3*time.Minute, "bounded per-case timeout") - if err := flags.Parse(arguments); err != nil { - return nativeE0Config{}, err - } - if flags.NArg() != 0 || *fixture == "" || *machinePath == "" || *timeout <= 0 || *timeout > 10*time.Minute { - return nativeE0Config{}, errors.New("native formal E0 requires only fixture, machine descriptor, and a bounded timeout") - } - config := nativeE0Config{fixturePath: filepath.Clean(*fixture), machinePath: filepath.Clean(*machinePath), timeout: *timeout} - for _, path := range []string{config.fixturePath, config.machinePath} { - if !filepath.IsAbs(path) { - return nativeE0Config{}, errors.New("native formal E0 file paths must be canonical and absolute") - } - } - return config, nil -} - -func prepareNativeE0Inputs(config nativeE0Config) (nativeE0Config, error) { - want, err := e0selftest.InputPaths() - if err != nil { - return nativeE0Config{}, err - } - consumed := []string{config.fixturePath, config.machinePath} - slices.Sort(consumed) - if !slices.Equal(consumed, want) { - return nativeE0Config{}, fmt.Errorf("native typed self-test flags consume %v, E0 invocation pins %v", consumed, want) - } - fixtureRaw, err := readFixture(config.fixturePath) - if err != nil { - return nativeE0Config{}, err - } - config.fixture, err = rq2fixture.Decode(fixtureRaw) - if err != nil { - return nativeE0Config{}, err - } - if err := config.fixture.RequireOperations(nativeE0Operations); err != nil { - return nativeE0Config{}, err - } - for _, backend := range []string{"kzg", "ipa"} { - if _, err := config.fixture.Root(backend); err != nil { - return nativeE0Config{}, err - } - } - config.machinePin, err = rq2e0.DescribeRegularFile("rq2-native-machine-descriptor", config.machinePath) - if err != nil { - return nativeE0Config{}, err - } - config.machine, err = machine.LoadPinned(config.machinePath, config.machinePin.SHA256, config.machinePin.Bytes) - if err != nil { - return nativeE0Config{}, err - } - return config, nil -} - -func executeNativeE0Matrix(config nativeE0Config, backend, caseID string) error { - execution, err := startNativeE0Execution(config, backend, caseID, config.fixturePath, "") - if err != nil { - return err - } - defer execution.abort() - if err := runNativeE0Session(execution.harness, config.fixture, backend, execution.root, nativeE0Operations, caseID); err != nil { - return err - } - if err := execution.harness.Close(); err != nil { - return err - } - if operations := execution.gateway.Operations(); operations != uint64(len(nativeE0Operations)) { - return fmt.Errorf("conformance Gateway observed %d exact-root submissions, want %d", operations, len(nativeE0Operations)) - } - return nil -} - -func executeNativeE0Hostile(config nativeE0Config, caseID string) error { - switch caseID { - case "reject-malformed-request": - valid, err := json.Marshal(nativeE0Request(config.fixture.FixtureID, "kzg", caseID, "preflight", rq2wire.RecordPreflight, "", false, "")) - if err != nil { - return err - } - duplicate := bytes.Replace(valid, []byte(`"worker_id":`), []byte(`"worker_id":"duplicate","worker_id":`), 1) - for _, hostile := range []struct { - id string - raw []byte - }{ - {id: "unknown-field", raw: []byte(`{"schema_version":"malt-rq2-worker-request/v1","unknown":true}`)}, - {id: "duplicate-field", raw: duplicate}, - } { - execution, err := startNativeE0Execution(config, "kzg", caseID+"-"+hostile.id, config.fixturePath, "") - if err != nil { - return err - } - if err := execution.harness.WriteRaw(hostile.raw); err != nil { - execution.abort() - return err - } - if err := execution.harness.WaitError(); err != nil { - execution.abort() - return err - } - if err := requireNoNativeE0Submissions(execution); err != nil { - execution.abort() - return err - } - execution.abort() - } - return nil - case "reject-backend-root-substitution": - return withNativeHostileHarness(config, caseID, func(execution *nativeE0Execution) error { - ipaRoot, err := config.fixture.Root("ipa") - if err != nil { - return err - } - if err := nativeE0Preflight(execution.harness, config.fixture.FixtureID, "kzg", caseID); err != nil { - return err - } - request := nativeE0Request(config.fixture.FixtureID, "kzg", caseID, "session-start", rq2wire.RecordSessionStart, "", false, ipaRoot.String()) - if request.Validate() == nil { - return errors.New("backend substitution unexpectedly satisfies the current request contract") - } - raw, err := json.Marshal(request) - if err != nil { - return err - } - if err := execution.harness.WriteRaw(raw); err != nil { - return err - } - if err := execution.harness.WaitError(); err != nil { - return err - } - return requireNoNativeE0Submissions(execution) - }) - case "reject-stale-session-root": - return withNativeHostileHarness(config, caseID, func(execution *nativeE0Execution) error { - if err := nativeE0Preflight(execution.harness, config.fixture.FixtureID, "kzg", caseID); err != nil { - return err - } - if err := nativeE0StartSession(execution.harness, config.fixture.FixtureID, "kzg", caseID, execution.root); err != nil { - return err - } - initialRoot := execution.root - first := nativeE0Request(config.fixture.FixtureID, "kzg", caseID, "mutation-current", rq2wire.RecordMutation, nativeE0Operations[0], true, initialRoot) - firstRecord, err := nativeE0Exchange(execution.harness, first, true, true) - if err != nil { - return err - } - if firstRecord.Mutation == nil { - return errors.New("stale-root setup mutation omitted mutation evidence") - } - request := nativeE0Request(config.fixture.FixtureID, "kzg", caseID, "mutation-stale", rq2wire.RecordMutation, nativeE0Operations[1], true, initialRoot) - record, err := nativeE0Exchange(execution.harness, request, true, false) - if err != nil { - return err - } - if record.Success || record.FailureClass != "root_continuity" { - return fmt.Errorf("stale accepted root was not rejected: %+v", record) - } - if operations := execution.gateway.Operations(); operations != 1 { - return fmt.Errorf("stale-root hostile case submitted %d client roots, want 1 setup mutation", operations) - } - return nil - }) - case "reject-tampered-fixture": - tampered, err := tamperedNativeE0Fixture(config) - if err != nil { - return err - } - defer os.Remove(tampered) - return expectNativeWorkerStartupFailure(config, caseID, tampered, "") - case "reject-machine-descriptor-pin-substitution": - return expectNativeWorkerStartupFailure(config, caseID, config.fixturePath, strings.Repeat("0", 64)) - default: - return fmt.Errorf("unknown native E0 hostile case %q", caseID) - } -} - -func withNativeHostileHarness(config nativeE0Config, caseID string, execute func(*nativeE0Execution) error) error { - execution, err := startNativeE0Execution(config, "kzg", caseID, config.fixturePath, "") - if err != nil { - return err - } - defer execution.abort() - return execute(execution) -} - -func expectNativeWorkerStartupFailure(config nativeE0Config, caseID, fixturePath, overrideMachineSHA string) error { - execution, err := startNativeE0Execution(config, "kzg", caseID, fixturePath, overrideMachineSHA) - if err != nil { - return err - } - defer execution.abort() - request := nativeE0Request(config.fixture.FixtureID, "kzg", caseID, "preflight", rq2wire.RecordPreflight, "", false, "") - var record rq2wire.WorkerRecord - if err := execution.harness.Exchange(request, &record); err == nil { - return fmt.Errorf("hostile native startup unexpectedly emitted a record: %+v", record) - } - if err := execution.harness.WaitError(); err != nil { - return err - } - return requireNoNativeE0Submissions(execution) -} - -func startNativeE0Execution(config nativeE0Config, backend, caseID, fixturePath, overrideMachineSHA string) (*nativeE0Execution, error) { - tokenDigest := sha256.Sum256([]byte(nativeE0CapabilityID + "\x00" + backend + "\x00" + caseID)) - token := hex.EncodeToString(tokenDigest[:]) - gateway, root, err := rq2e0.NewConformanceGateway(config.fixture, backend, token) - if err != nil { - return nil, err - } - machineSHA := config.machinePin.SHA256 - if overrideMachineSHA != "" { - machineSHA = overrideMachineSHA - } - args := []string{ - "-gateway-base-url", gateway.URL(), "-fixture", fixturePath, "-worker-id", "e0-worker-" + shortNativeE0ID(caseID), - "-platform-id", "e0-native", "-client-kind", rq2wire.ClientNative, "-backend", backend, "-lifecycle", rq2wire.LifecycleNativeLong, - "-low-power-arm=" + strconv.FormatBool(config.machine.Descriptor.LowPowerARM()), "-gateway-instance-token", token, - "-machine-descriptor", config.machinePath, "-machine-descriptor-sha256", machineSHA, - "-machine-descriptor-bytes", strconv.FormatInt(config.machinePin.Bytes, 10), "-request-timeout", config.timeout.String(), - } - harness, err := rq2e0.StartHarness(config.timeout, func(input io.Reader, output io.Writer) error { - return run(args, input, output, io.Discard) - }) - if err != nil { - gateway.Close() - return nil, err - } - return &nativeE0Execution{gateway: gateway, harness: harness, root: root.String()}, nil -} - -func requireNoNativeE0Submissions(execution *nativeE0Execution) error { - if operations := execution.gateway.Operations(); operations != 0 { - return fmt.Errorf("hostile case submitted %d client roots", operations) - } - return nil -} - -func runNativeE0Session(harness *rq2e0.Harness, fixture *rq2fixture.Fixture, backend, root string, operations []string, caseID string) error { - if err := nativeE0Preflight(harness, fixture.FixtureID, backend, caseID); err != nil { - return err - } - if err := nativeE0StartSession(harness, fixture.FixtureID, backend, caseID, root); err != nil { - return err - } - accepted := root - for index, operation := range operations { - request := nativeE0Request(fixture.FixtureID, backend, caseID, fmt.Sprintf("mutation-%02d", index), rq2wire.RecordMutation, operation, true, accepted) - record, err := nativeE0Exchange(harness, request, true, true) - if err != nil { - return err - } - if !record.Success || record.Mutation == nil || !record.Mutation.ReceiptAccepted || record.Mutation.Operation != operation || record.Mutation.PriorRoot != accepted || record.Mutation.ReceiptRoot == accepted || - !record.Mutation.Metrics.MutationTotal.Applicable || record.Mutation.Metrics.MutationTotal.DurationNS == 0 || - !record.Mutation.Metrics.CandidateGeneration.Applicable || !record.Mutation.Metrics.GatewayPersist.Applicable || !record.Mutation.Metrics.ReceiptCheck.Applicable { - return fmt.Errorf("native production mutation %q returned incomplete evidence: %+v", operation, record) - } - accepted = record.Mutation.ReceiptRoot - } - request := nativeE0Request(fixture.FixtureID, backend, caseID, "session-end", rq2wire.RecordSessionEnd, "", false, accepted) - ended, err := nativeE0Exchange(harness, request, true, true) - if err != nil { - return err - } - if !ended.Success || ended.Session == nil || ended.Session.AcceptedRoot != accepted || ended.Session.ReceiptCount != uint64(len(operations)) || !ended.Session.AuditPassed { - return fmt.Errorf("native production session-end audit failed: %+v", ended) - } - return nil -} - -func nativeE0Preflight(harness *rq2e0.Harness, fixtureID, backend, caseID string) error { - request := nativeE0Request(fixtureID, backend, caseID, "preflight", rq2wire.RecordPreflight, "", false, "") - record, err := nativeE0Exchange(harness, request, true, true) - if err != nil { - return err - } - if !record.Success || record.Runtime == nil || !slices.Contains(record.Capabilities, "native-writer-v1") || !slices.Contains(record.Capabilities, "long-lived-session-v1") { - return fmt.Errorf("native production preflight failed: %+v", record) - } - return nil -} - -func nativeE0StartSession(harness *rq2e0.Harness, fixtureID, backend, caseID, root string) error { - request := nativeE0Request(fixtureID, backend, caseID, "session-start", rq2wire.RecordSessionStart, "", false, root) - record, err := nativeE0Exchange(harness, request, true, true) - if err != nil { - return err - } - if !record.Success || record.Session == nil || record.Session.AcceptedRoot != root { - return fmt.Errorf("native production session did not start from exact root: %+v", record) - } - return nil -} - -func nativeE0Exchange(harness *rq2e0.Harness, request rq2wire.WorkerRequest, requireValidRequest, requireSuccess bool) (rq2wire.WorkerRecord, error) { - requestErr := request.Validate() - if requireValidRequest && requestErr != nil { - return rq2wire.WorkerRecord{}, requestErr - } - if !requireValidRequest && requestErr == nil { - return rq2wire.WorkerRecord{}, errors.New("native E0 hostile request unexpectedly satisfies the shared wire contract") - } - var record rq2wire.WorkerRecord - if err := harness.Exchange(request, &record); err != nil { - return rq2wire.WorkerRecord{}, err - } - if err := rq2wire.Bind(record, request); err != nil { - return rq2wire.WorkerRecord{}, err - } - if err := record.Validate(); err != nil { - return rq2wire.WorkerRecord{}, err - } - if requireSuccess && !record.Success { - return rq2wire.WorkerRecord{}, fmt.Errorf("native worker failed: %s: %s", record.FailureClass, record.Error) - } - return record, nil -} - -func nativeE0Request(fixtureID, backend, caseID, requestID, kind, operation string, measured bool, root string) rq2wire.WorkerRequest { - return rq2wire.WorkerRequest{ - SchemaVersion: rq2wire.WorkerRequestSchema, WorkerID: "e0-worker-" + shortNativeE0ID(caseID), RequestID: requestID, - RecordKind: kind, SessionID: "e0-session-" + shortNativeE0ID(caseID), ClientKind: rq2wire.ClientNative, - PlatformID: "e0-native", Backend: backend, Lifecycle: rq2wire.LifecycleNativeLong, FixtureID: fixtureID, - Operation: operation, Measured: measured, ExpectedAcceptedRoot: root, - } -} - -func tamperedNativeE0Fixture(config nativeE0Config) (string, error) { - raw, err := json.Marshal(config.fixture) - if err != nil { - return "", err - } - fixture, err := rq2fixture.Decode(raw) - if err != nil { - return "", err - } - switch { - case len(fixture.DirectFiles) > 0 && len(fixture.DirectFiles[0].Bytes) > 0: - fixture.DirectFiles[0].Bytes[0] ^= 0xff - case len(fixture.ListFiles) > 0 && len(fixture.ListFiles[0].Chunks) > 0 && len(fixture.ListFiles[0].Chunks[0].Bytes) > 0: - fixture.ListFiles[0].Chunks[0].Bytes[0] ^= 0xff - default: - return "", errors.New("native E0 fixture has no payload byte to tamper") - } - tampered, err := json.Marshal(fixture) - if err != nil { - return "", err - } - file, err := os.CreateTemp(filepath.Dir(config.fixturePath), "rq2-native-e0-tampered-*.json") - if err != nil { - return "", err - } - name := file.Name() - if _, err := file.Write(tampered); err != nil { - _ = file.Close() - _ = os.Remove(name) - return "", err - } - if err := file.Close(); err != nil { - _ = os.Remove(name) - return "", err - } - return name, nil -} - -func shortNativeE0ID(value string) string { - digest := sha256.Sum256([]byte(value)) - return hex.EncodeToString(digest[:6]) -} - -func nativeE0ProfileDigest() string { - contract, err := nativeE0Profile.Contract() - if err != nil { - return "" - } - return contract.ProfileSHA256 -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-worker/selftest_test.go b/tools/evaluation/cmd/malt-eval-rq2-worker/selftest_test.go deleted file mode 100644 index fceb36c..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-worker/selftest_test.go +++ /dev/null @@ -1,211 +0,0 @@ -package main - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "os" - "path/filepath" - "testing" - - "github.com/dewebprotocol/malt-client/internal/evaluation/e0selftest" - "github.com/dewebprotocol/malt-client/internal/evaluation/machine" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2e0" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2fixture" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq2wire" -) - -func TestNativeFormalE0RunsProductionKZGAndIPAMatrices(t *testing.T) { - fixturePath := writeNativeE0Fixture(t) - machinePath := writeNativeE0MachineDescriptor(t) - executable, err := os.Executable() - if err != nil { - t.Fatal(err) - } - environment, err := e0selftest.BuildInvocationEnvironment( - nativeE0CapabilityID, - nativeE0Profile, - e0selftest.InputFile{FileID: "rq2-native-writer", Path: executable}, - []e0selftest.InputFile{ - {FileID: "rq2-native-source-fixture", Path: fixturePath}, - {FileID: "rq2-native-machine-descriptor", Path: machinePath}, - }, - []string{"rq2-native-source-fixture"}, - ) - if err != nil { - t.Fatal(err) - } - t.Setenv(e0selftest.InvocationEnvironment, environment) - var output bytes.Buffer - if err := runNativeE0SelfTest([]string{ - "-fixture", fixturePath, - "-machine-descriptor", machinePath, - "-request-timeout", "30s", - }, &output, &bytes.Buffer{}); err != nil { - t.Fatal(err) - } - var receipt e0selftest.Receipt - if err := json.Unmarshal(output.Bytes(), &receipt); err != nil { - t.Fatal(err) - } - if err := receipt.Validate(); err != nil { - t.Fatal(err) - } - if receipt.CapabilityID != nativeE0CapabilityID || receipt.ProfileSHA256 != nativeE0ExpectedProfileSHA256 || - receipt.Positive.Executed != 2 || receipt.Hostile.Executed != 5 || len(receipt.TestedInputs) != 2 { - t.Fatalf("native E0 receipt = %+v", receipt) - } -} - -func TestNativeFormalE0ConsumesOnlyFixtureAndMachineDescriptor(t *testing.T) { - fixturePath := writeNativeE0Fixture(t) - machinePath := writeNativeE0MachineDescriptor(t) - extraPath := filepath.Join(t.TempDir(), "undeclared-runtime-input") - if err := os.WriteFile(extraPath, []byte("must not be silently accepted"), 0o600); err != nil { - t.Fatal(err) - } - executable, err := os.Executable() - if err != nil { - t.Fatal(err) - } - environment, err := e0selftest.BuildInvocationEnvironment( - nativeE0CapabilityID, - nativeE0Profile, - e0selftest.InputFile{FileID: "rq2-native-writer", Path: executable}, - []e0selftest.InputFile{ - {FileID: "rq2-native-source-fixture", Path: fixturePath}, - {FileID: "rq2-native-machine-descriptor", Path: machinePath}, - {FileID: "rq2-native-undeclared-input", Path: extraPath}, - }, - []string{"rq2-native-source-fixture"}, - ) - if err != nil { - t.Fatal(err) - } - t.Setenv(e0selftest.InvocationEnvironment, environment) - err = runNativeE0SelfTest([]string{"-fixture", fixturePath, "-machine-descriptor", machinePath}, &bytes.Buffer{}, &bytes.Buffer{}) - if err == nil { - t.Fatal("native formal E0 accepted an invocation input with no typed consumer") - } - if _, err := parseNativeE0Flags([]string{ - "-fixture", fixturePath, - "-machine-descriptor", machinePath, - "-gateway-controller", "/tmp/controller", - }, &bytes.Buffer{}); err == nil { - t.Fatal("native formal E0 accepted retired Gateway-controller flags") - } -} - -func TestNativeE0ProfileDigestIsStable(t *testing.T) { - if digest := nativeE0ProfileDigest(); digest != nativeE0ExpectedProfileSHA256 { - t.Fatalf("native E0 profile digest = %s", digest) - } -} - -func TestNativeLocalRequestProtocolMatchesSharedRQ2Wire(t *testing.T) { - fixtureRaw, err := os.ReadFile(writeNativeE0Fixture(t)) - if err != nil { - t.Fatal(err) - } - fixture, err := rq2fixture.Decode(fixtureRaw) - if err != nil { - t.Fatal(err) - } - root, err := fixture.Root("kzg") - if err != nil { - t.Fatal(err) - } - shared := nativeE0Request(fixture.FixtureID, "kzg", "wire-compatibility", "session-start", rq2wire.RecordSessionStart, "", false, root.String()) - if err := shared.Validate(); err != nil { - t.Fatal(err) - } - raw, err := json.Marshal(shared) - if err != nil { - t.Fatal(err) - } - local, err := decodeWorkerRequest(raw) - if err != nil { - t.Fatalf("native local decoder rejected shared request: %v", err) - } - localRaw, err := json.Marshal(local) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(raw, localRaw) { - t.Fatalf("native local request JSON %s differs from shared JSON %s", localRaw, raw) - } -} - -func writeNativeE0Fixture(t *testing.T) string { - t.Helper() - seed := sha256.Sum256([]byte("native formal E0 fixture mutation seed")) - listIndex := uint64(1) - chunkA := make([]byte, 32) - chunkB := make([]byte, 32) - copy(chunkA, "native-e0-list-chunk-a") - copy(chunkB, "native-e0-list-chunk-b") - source := &rq2fixture.SourceDefinition{ - SchemaVersion: rq2fixture.SourceSchemaVersion, - FixtureID: "rq2-native-formal-e0", - MutationSeedSHA256: hex.EncodeToString(seed[:]), - DirectFiles: []rq2fixture.SourceDirectFile{ - {Path: "docs/modify.txt", Coordinate: "docs/modify.txt", Bytes: []byte("modify source")}, - {Path: "docs/delete.txt", Coordinate: "docs/delete.txt", Bytes: []byte("delete source")}, - {Path: "docs/move-source.txt", Coordinate: "docs/move-source.txt", Bytes: []byte("move source")}, - {Path: "docs/rename-source.txt", Coordinate: "docs/rename-source.txt", Bytes: []byte("rename source")}, - }, - ListFiles: []rq2fixture.SourceListFile{{ - Path: "data/large.bin", Coordinate: "data/large.bin", ChunkSize: 32, TotalSize: 64, - Chunks: []rq2fixture.SourceListChunk{{Index: 0, Bytes: chunkA}, {Index: 1, Bytes: chunkB}}, - }}, - Operations: []rq2fixture.Operation{ - {Name: "append", Kind: rq2fixture.KindListAppend, SourcePath: "data/large.bin", SourceCoordinate: "data/large.bin", PayloadBytes: 32}, - {Name: "batch-sync", Kind: rq2fixture.KindBatchInsert, Batch: []rq2fixture.BatchTarget{ - {Path: "batch/one.txt", Coordinate: "batch/one.txt", PayloadBytes: 16}, - {Path: "batch/two.txt", Coordinate: "batch/two.txt", PayloadBytes: 20}, - }}, - {Name: "create-small-file", Kind: rq2fixture.KindDirectInsert, DestinationPath: "created/small.txt", DestinationCoordinate: "created/small.txt", PayloadBytes: 32}, - {Name: "delete-directory-entry", Kind: rq2fixture.KindDirectDelete, SourcePath: "docs/delete.txt", SourceCoordinate: "docs/delete.txt"}, - {Name: "insert-directory-entry", Kind: rq2fixture.KindDirectInsert, DestinationPath: "inserted/entry.txt", DestinationCoordinate: "inserted/entry.txt", PayloadBytes: 24}, - {Name: "modify-small-file", Kind: rq2fixture.KindDirectReplace, SourcePath: "docs/modify.txt", SourceCoordinate: "docs/modify.txt", PayloadBytes: 48}, - {Name: "move", Kind: rq2fixture.KindDirectMove, SourcePath: "docs/move-source.txt", SourceCoordinate: "docs/move-source.txt", DestinationPath: "moved/destination.txt", DestinationCoordinate: "moved/destination.txt"}, - {Name: "rename", Kind: rq2fixture.KindDirectMove, SourcePath: "docs/rename-source.txt", SourceCoordinate: "docs/rename-source.txt", DestinationPath: "docs/renamed.txt", DestinationCoordinate: "docs/renamed.txt"}, - {Name: "replace-large-file-chunk", Kind: rq2fixture.KindListReplace, SourcePath: "data/large.bin", SourceCoordinate: "data/large.bin", PayloadBytes: 32, ListIndex: &listIndex}, - }, - } - fixture, err := rq2e0.BuildFixture(t.Context(), source) - if err != nil { - t.Fatal(err) - } - raw, err := json.Marshal(fixture) - if err != nil { - t.Fatal(err) - } - path := filepath.Join(t.TempDir(), "rq2-native-fixture.json") - if err := os.WriteFile(path, raw, 0o600); err != nil { - t.Fatal(err) - } - return path -} - -func writeNativeE0MachineDescriptor(t *testing.T) string { - t.Helper() - identity, err := machine.Probe() - if err != nil { - t.Fatal(err) - } - descriptor, err := machine.NewDescriptor("rq2-native-e0-host", machine.ClassGeneral, "test-suite:live-machine-probe", identity) - if err != nil { - t.Fatal(err) - } - raw, err := json.Marshal(descriptor) - if err != nil { - t.Fatal(err) - } - path := filepath.Join(t.TempDir(), "rq2-native-machine.json") - if err := os.WriteFile(path, raw, 0o600); err != nil { - t.Fatal(err) - } - return path -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-worker/usage_other.go b/tools/evaluation/cmd/malt-eval-rq2-worker/usage_other.go deleted file mode 100644 index 251be22..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-worker/usage_other.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris - -package main - -import "fmt" - -type processUsage struct { - cpuNS uint64 - peakRSSBytes uint64 -} - -func readProcessUsage() (processUsage, error) { - return processUsage{}, fmt.Errorf("process CPU and peak RSS accounting is unavailable on this platform") -} diff --git a/tools/evaluation/cmd/malt-eval-rq2-worker/usage_unix.go b/tools/evaluation/cmd/malt-eval-rq2-worker/usage_unix.go deleted file mode 100644 index 6edf3e8..0000000 --- a/tools/evaluation/cmd/malt-eval-rq2-worker/usage_unix.go +++ /dev/null @@ -1,32 +0,0 @@ -//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris - -package main - -import "golang.org/x/sys/unix" - -type processUsage struct { - cpuNS uint64 - peakRSSBytes uint64 -} - -func readProcessUsage() (processUsage, error) { - var value unix.Rusage - if err := unix.Getrusage(unix.RUSAGE_SELF, &value); err != nil { - return processUsage{}, err - } - cpu := timevalNS(value.Utime) + timevalNS(value.Stime) - peak := uint64(value.Maxrss) - // Linux and the BSDs report ru_maxrss in KiB. Darwin reports bytes. - // Go's darwin Rusage shape is shared, so retain the documented exception. - if unixRSSIsKiB { - peak *= 1024 - } - return processUsage{cpuNS: cpu, peakRSSBytes: peak}, nil -} - -func timevalNS(value unix.Timeval) uint64 { - if value.Sec < 0 || value.Usec < 0 { - return 0 - } - return uint64(value.Sec)*1_000_000_000 + uint64(value.Usec)*1_000 -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-hash-worker/main.go b/tools/evaluation/cmd/malt-eval-rq3-hash-worker/main.go deleted file mode 100644 index 9155748..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-hash-worker/main.go +++ /dev/null @@ -1,346 +0,0 @@ -// Command malt-eval-rq3-hash-worker exposes the MALT runtime UnixFS/HAMT -// baseline primitive as strict, bounded JSONL over stdin/stdout. -package main - -import ( - "bufio" - "bytes" - "context" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "os" - "os/signal" - "reflect" - "strings" - "unicode/utf8" - - "github.com/dewebprotocol/malt-client/internal/evaluation/rq3baseline" -) - -const ( - maxJSONLRecordBytes = rq3baseline.MaximumJSONLRecordBytes - maxWorkerRequests = 4_096 -) - -func main() { - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) - defer stop() - if err := run(ctx, os.Args[1:], os.Stdin, os.Stdout); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func run(ctx context.Context, arguments []string, input io.Reader, output io.Writer) error { - flags := flag.NewFlagSet("malt-eval-rq3-hash-worker", flag.ContinueOnError) - flags.SetOutput(io.Discard) - selfTestCorpus := flags.String("self-test-corpus", "", "formal E0 hash-adapter corpus") - var boundWorkloads, gitExecutables repeatedPathFlag - flags.Var(&boundWorkloads, "bound-workload", "production RQ3 workload artifact bound by formal E0 (repeatable)") - flags.Var(&gitExecutables, "git-executable", "production Git executable bound by formal E0 (repeatable)") - if err := flags.Parse(arguments); err != nil { - return err - } - if flags.NArg() != 0 { - return fmt.Errorf("malt-eval-rq3-hash-worker accepts no positional arguments") - } - if *selfTestCorpus != "" { - return runHashAdapterSelfTest(ctx, *selfTestCorpus, boundWorkloads, gitExecutables, output) - } - if len(arguments) != 0 { - return fmt.Errorf("normal hash-worker mode accepts no command-line arguments") - } - return runWorker(ctx, input, output) -} - -func runWorker(ctx context.Context, input io.Reader, output io.Writer) (returnErr error) { - scanner := bufio.NewScanner(input) - scanner.Buffer(make([]byte, 64<<10), maxJSONLRecordBytes) - encoder := json.NewEncoder(output) - encoder.SetEscapeHTML(false) - requests := 0 - handler := workerHandler{} - defer func() { - if handler.stream != nil { - returnErr = errors.Join(returnErr, handler.stream.Close()) - } - }() - for scanner.Scan() { - requests++ - if requests > maxWorkerRequests { - return fmt.Errorf("RQ3 hash worker exceeds %d-request process limit", maxWorkerRequests) - } - if err := ctx.Err(); err != nil { - return err - } - response := handler.handleLine(ctx, scanner.Bytes()) - if err := encoder.Encode(response); err != nil { - return fmt.Errorf("encode worker response: %w", err) - } - } - if err := scanner.Err(); err != nil { - return fmt.Errorf("read JSONL request (maximum record %d bytes): %w", maxJSONLRecordBytes, err) - } - if handler.stream != nil { - return fmt.Errorf("RQ3 hash worker ended before stream-finish") - } - if requests < 2 || !handler.terminal { - return fmt.Errorf("RQ3 hash worker ended before a complete run") - } - return nil -} - -type workerHandler struct { - stream *rq3baseline.StreamSession - system string - layout rq3baseline.LayoutSpec - applied uint32 - requests int - capabilityPassed bool - terminal bool -} - -func handleLine(ctx context.Context, line []byte) rq3baseline.WorkerResponse { - return (&workerHandler{}).handleLine(ctx, line) -} - -func (h *workerHandler) handleLine(ctx context.Context, line []byte) rq3baseline.WorkerResponse { - h.requests++ - request, err := decodeRequest(line) - if err != nil { - if h.stream != nil { - err = errors.Join(err, h.poison()) - } - return errorResponse("", "invalid_request", err, "") - } - if err := validateEnvelope(request); err != nil { - if h.stream != nil { - err = errors.Join(err, h.poison()) - } - return errorResponse(request.RequestID, "invalid_request", err, "") - } - if h.terminal { - return errorResponse(request.RequestID, "invalid_request", errors.New("worker run is already complete or poisoned"), "") - } - switch request.Operation { - case rq3baseline.OperationCapabilities: - if h.requests != 1 || h.capabilityPassed { - h.terminal = true - return errorResponse(request.RequestID, "invalid_request", errors.New("capabilities must be the first request"), "") - } - capability := rq3baseline.Capability() - h.capabilityPassed = true - return rq3baseline.WorkerResponse{ - SchemaVersion: rq3baseline.WorkerResponseSchema, - RequestID: request.RequestID, - OK: true, - Capability: &capability, - } - case rq3baseline.OperationRun: - if !h.capabilityPassed || h.requests != 2 || h.stream != nil { - return errorResponse(request.RequestID, "invalid_request", errors.New("one-shot run cannot overlap a stream"), "") - } - h.terminal = true - result, err := rq3baseline.Run(ctx, *request.Run) - if err != nil { - var unsupported *rq3baseline.UnsupportedError - if errors.As(err, &unsupported) { - return errorResponse(request.RequestID, "unsupported_capability", err, unsupported.Gap) - } - return errorResponse(request.RequestID, "invalid_or_failed_run", err, "") - } - return rq3baseline.WorkerResponse{ - SchemaVersion: rq3baseline.WorkerResponseSchema, - RequestID: request.RequestID, - OK: true, - Result: result, - } - case rq3baseline.OperationStreamStart: - if !h.capabilityPassed || h.requests != 2 || h.stream != nil { - return errorResponse(request.RequestID, "invalid_request", errors.New("hash stream is already active"), "") - } - session, record, err := rq3baseline.StartStream(ctx, *request.Run) - if err != nil { - h.terminal = true - return errorResponse(request.RequestID, "invalid_or_failed_stream_start", err, "") - } - h.stream, h.system, h.layout, h.applied = session, request.Run.System, request.Run.Layout, 1 - return rq3baseline.WorkerResponse{SchemaVersion: rq3baseline.WorkerResponseSchema, RequestID: request.RequestID, OK: true, Stream: &rq3baseline.StreamResult{System: h.system, Layout: h.layout, Records: []rq3baseline.CommitRecord{record}, Root: session.Root(), CommitsApplied: h.applied}} - case rq3baseline.OperationStreamChunk: - if !h.capabilityPassed || h.stream == nil || request.Run.System != h.system || !reflect.DeepEqual(request.Run.Layout, h.layout) || request.Run.Snapshot.CommitID != "" || request.Run.Snapshot.Files != nil { - if h.stream != nil { - return errorResponse(request.RequestID, "invalid_request", errors.Join(errors.New("hash stream chunk does not bind the active system/layout or carries a snapshot"), h.poison()), "") - } - return errorResponse(request.RequestID, "invalid_request", errors.New("hash stream chunk does not bind the active system/layout or carries a snapshot"), "") - } - records, err := h.stream.ApplyChunk(ctx, request.Run.Commits) - if err != nil { - closeErr := h.stream.Close() - if closeErr == nil { - h.stream = nil - } - h.terminal = true - return errorResponse(request.RequestID, "invalid_or_failed_stream_chunk", errors.Join(err, closeErr), "") - } - h.applied += uint32(len(records)) - return rq3baseline.WorkerResponse{SchemaVersion: rq3baseline.WorkerResponseSchema, RequestID: request.RequestID, OK: true, Stream: &rq3baseline.StreamResult{System: h.system, Layout: h.layout, Records: records, Root: h.stream.Root(), CommitsApplied: h.applied}} - case rq3baseline.OperationStreamFinish: - if !h.capabilityPassed || h.stream == nil { - return errorResponse(request.RequestID, "invalid_request", errors.New("hash stream is not active"), "") - } - commitListSHA256, err := h.stream.CommitListSHA256() - if err != nil { - closeErr := h.stream.Close() - if closeErr == nil { - h.stream = nil - } - h.terminal = true - return errorResponse(request.RequestID, "invalid_or_failed_stream_finish", errors.Join(err, closeErr), "") - } - stream := &rq3baseline.StreamResult{System: h.system, Layout: h.layout, Records: []rq3baseline.CommitRecord{}, Root: h.stream.Root(), CommitsApplied: h.applied, Complete: true, CommitListSHA256: commitListSHA256} - if err := h.stream.Close(); err != nil { - h.terminal = true - return errorResponse(request.RequestID, "invalid_or_failed_stream_finish", err, "") - } - h.stream = nil - h.terminal = true - return rq3baseline.WorkerResponse{SchemaVersion: rq3baseline.WorkerResponseSchema, RequestID: request.RequestID, OK: true, Stream: stream} - default: - panic("validated operation is not handled") - } -} - -func (h *workerHandler) poison() error { - var err error - if h.stream != nil { - err = h.stream.Close() - if err == nil { - h.stream = nil - } - } - h.terminal = true - return err -} - -func decodeRequest(line []byte) (rq3baseline.WorkerRequest, error) { - if len(bytes.TrimSpace(line)) == 0 { - return rq3baseline.WorkerRequest{}, fmt.Errorf("JSONL record is empty") - } - if err := rejectDuplicateKeys(line); err != nil { - return rq3baseline.WorkerRequest{}, err - } - decoder := json.NewDecoder(bytes.NewReader(line)) - decoder.DisallowUnknownFields() - var request rq3baseline.WorkerRequest - if err := decoder.Decode(&request); err != nil { - return rq3baseline.WorkerRequest{}, fmt.Errorf("decode strict worker request: %w", err) - } - var extra any - if err := decoder.Decode(&extra); err != io.EOF { - if err == nil { - return rq3baseline.WorkerRequest{}, fmt.Errorf("worker request contains a trailing JSON value") - } - return rq3baseline.WorkerRequest{}, fmt.Errorf("decode worker request trailer: %w", err) - } - return request, nil -} - -func rejectDuplicateKeys(data []byte) error { - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.UseNumber() - // Only inspect the first value here. The strict typed decoder below owns - // unknown-field and trailing-value diagnostics; this pass exists solely to - // close encoding/json's otherwise last-key-wins duplicate-key behavior. - return scanJSONValue(decoder, "$") -} - -func scanJSONValue(decoder *json.Decoder, location string) error { - token, err := decoder.Token() - if err != nil { - return err - } - delimiter, ok := token.(json.Delim) - if !ok { - return nil - } - switch delimiter { - case '{': - seen := make(map[string]struct{}) - for decoder.More() { - keyToken, err := decoder.Token() - if err != nil { - return err - } - key, ok := keyToken.(string) - if !ok { - return fmt.Errorf("%s has a non-string object key", location) - } - if _, duplicate := seen[key]; duplicate { - return fmt.Errorf("duplicate JSON key %q at %s", key, location) - } - seen[key] = struct{}{} - if err := scanJSONValue(decoder, location+"."+key); err != nil { - return err - } - } - _, err = decoder.Token() - return err - case '[': - for index := 0; decoder.More(); index++ { - if err := scanJSONValue(decoder, fmt.Sprintf("%s[%d]", location, index)); err != nil { - return err - } - } - _, err = decoder.Token() - return err - default: - return fmt.Errorf("unexpected JSON delimiter %q at %s", delimiter, location) - } -} - -func validateEnvelope(request rq3baseline.WorkerRequest) error { - if request.SchemaVersion != rq3baseline.WorkerRequestSchema { - return fmt.Errorf("schema_version must be %q", rq3baseline.WorkerRequestSchema) - } - if strings.TrimSpace(request.RequestID) != request.RequestID || request.RequestID == "" || len(request.RequestID) > 256 || !utf8.ValidString(request.RequestID) { - return fmt.Errorf("request_id must be a non-empty bounded canonical string") - } - for _, r := range request.RequestID { - if r < 0x20 || r == 0x7f { - return fmt.Errorf("request_id contains a control character") - } - } - switch request.Operation { - case rq3baseline.OperationCapabilities: - if request.Run != nil { - return fmt.Errorf("capabilities request must not include run") - } - case rq3baseline.OperationRun, rq3baseline.OperationStreamStart, rq3baseline.OperationStreamChunk: - if request.Run == nil { - return fmt.Errorf("%s request must include run", request.Operation) - } - case rq3baseline.OperationStreamFinish: - if request.Run != nil { - return fmt.Errorf("stream-finish request must omit run") - } - default: - return fmt.Errorf("unsupported operation %q", request.Operation) - } - return nil -} - -func errorResponse(requestID, code string, err error, gap string) rq3baseline.WorkerResponse { - return rq3baseline.WorkerResponse{ - SchemaVersion: rq3baseline.WorkerResponseSchema, - RequestID: requestID, - OK: false, - Error: &rq3baseline.WorkerError{ - Code: code, - Message: err.Error(), - CapabilityGap: gap, - }, - } -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-hash-worker/main_test.go b/tools/evaluation/cmd/malt-eval-rq3-hash-worker/main_test.go deleted file mode 100644 index fba56ec..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-hash-worker/main_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package main - -import ( - "bytes" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "strings" - "testing" - - "github.com/dewebprotocol/malt-client/internal/evaluation/rq3baseline" -) - -func TestWorkerServesCapabilitiesAndRunAsStrictJSONL(t *testing.T) { - capRequest := rq3baseline.WorkerRequest{ - SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "cap-1", Operation: rq3baseline.OperationCapabilities, - } - runRequest := rq3baseline.WorkerRequest{ - SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "run-1", Operation: rq3baseline.OperationRun, Run: testRunSpec(true), - } - var input bytes.Buffer - for _, request := range []rq3baseline.WorkerRequest{capRequest, runRequest} { - encoded, err := json.Marshal(request) - if err != nil { - t.Fatal(err) - } - input.Write(encoded) - input.WriteByte('\n') - } - var output bytes.Buffer - if err := runWorker(t.Context(), &input, &output); err != nil { - t.Fatal(err) - } - decoder := json.NewDecoder(&output) - var capResponse, runResponse rq3baseline.WorkerResponse - if err := decoder.Decode(&capResponse); err != nil { - t.Fatal(err) - } - if err := decoder.Decode(&runResponse); err != nil { - t.Fatal(err) - } - if !capResponse.OK || capResponse.Capability == nil || capResponse.Result != nil || capResponse.Capability.CapabilityID == "" { - t.Fatalf("capability response = %#v", capResponse) - } - if !runResponse.OK || runResponse.Result == nil || runResponse.Capability != nil || len(runResponse.Result.Records) != 1 { - t.Fatalf("run response = %#v", runResponse) - } -} - -func TestWorkerRejectsUnknownAndTrailingJSONButContinues(t *testing.T) { - input := strings.Join([]string{ - `{"schema_version":"malt-rq3-hash-worker-request/v1","request_id":"bad-1","operation":"capabilities","unknown":true}`, - `{"schema_version":"malt-rq3-hash-worker-request/v1","request_id":"bad-2","operation":"capabilities"} {}`, - `{"schema_version":"malt-rq3-hash-worker-request/v1","request_id":"bad-3","operation":"capabilities","run":{"system":"a","system":"b"}}`, - `{"schema_version":"malt-rq3-hash-worker-request/v1","request_id":"good","operation":"capabilities"}`, - }, "\n") + "\n" - var output bytes.Buffer - if err := runWorker(t.Context(), strings.NewReader(input), &output); err != nil { - t.Fatal(err) - } - decoder := json.NewDecoder(&output) - responses := make([]rq3baseline.WorkerResponse, 0, 4) - for range 4 { - var response rq3baseline.WorkerResponse - if err := decoder.Decode(&response); err != nil { - t.Fatal(err) - } - responses = append(responses, response) - } - if len(responses) != 4 { - t.Fatalf("responses = %d, output = %s", len(responses), output.String()) - } - if responses[0].OK || responses[0].Error == nil || !strings.Contains(responses[0].Error.Message, "unknown field") { - t.Fatalf("unknown-field response = %#v", responses[0]) - } - if responses[1].OK || responses[1].Error == nil || !strings.Contains(responses[1].Error.Message, "trailing JSON") { - t.Fatalf("trailing response = %#v", responses[1]) - } - if responses[2].OK || responses[2].Error == nil || !strings.Contains(responses[2].Error.Message, "duplicate JSON key") { - t.Fatalf("duplicate-key response = %#v", responses[2]) - } - if responses[3].OK || responses[3].Error == nil || responses[3].RequestID != "good" { - t.Fatalf("worker did not fail closed: %#v", responses[3]) - } -} - -func TestWorkerReportsFailClosedCapabilityGap(t *testing.T) { - request := rq3baseline.WorkerRequest{ - SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "unsupported", Operation: rq3baseline.OperationRun, Run: testRunSpec(true), - } - request.Run.Snapshot.Files[0].FileKind = "symlink" - encoded, err := json.Marshal(request) - if err != nil { - t.Fatal(err) - } - handler := new(workerHandler) - capability, err := json.Marshal(rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "cap", Operation: rq3baseline.OperationCapabilities}) - if err != nil { - t.Fatal(err) - } - if response := handler.handleLine(t.Context(), capability); !response.OK { - t.Fatalf("capability response = %#v", response) - } - response := handler.handleLine(t.Context(), encoded) - if response.OK || response.Error == nil || response.Error.Code != "unsupported_capability" || response.Error.CapabilityGap != "symlink_and_special_file_mutation" { - t.Fatalf("response = %#v", response) - } -} - -func TestWorkerRejectsSemanticallyInvalidEnvelope(t *testing.T) { - request := rq3baseline.WorkerRequest{ - SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "bad-envelope", Operation: rq3baseline.OperationCapabilities, Run: testRunSpec(true), - } - encoded, err := json.Marshal(request) - if err != nil { - t.Fatal(err) - } - response := handleLine(t.Context(), encoded) - if response.OK || response.RequestID != request.RequestID || response.Error == nil || response.Error.Code != "invalid_request" { - t.Fatalf("response = %#v", response) - } -} - -func testRunSpec(rawLeaves bool) *rq3baseline.RunSpec { - payload := []byte("payload") - digest := sha256.Sum256(payload) - mode := uint32(0o644) - return &rq3baseline.RunSpec{ - System: rq3baseline.SystemMerkleDAGUnixFS, - Layout: rq3baseline.LayoutSpec{ - Model: "unixfs", FileLayout: "balanced", DirectoryLayout: "basic", - Chunking: rq3baseline.ChunkingSpec{Algorithm: "fixed", SizeBytes: 4}, - HAMTFanout: 0, RawFileLeaf: &rawLeaves, - }, - Snapshot: rq3baseline.Snapshot{ - CommitID: "c1", - Files: []rq3baseline.FrozenFile{{ - Path: "file.txt", FileKind: rq3baseline.FileKindRegular, PayloadBase64: base64.StdEncoding.EncodeToString(payload), PayloadSHA256: hex.EncodeToString(digest[:]), Mode: &mode, - }}, - }, - Commits: []rq3baseline.Commit{}, - } -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-hash-worker/selftest.go b/tools/evaluation/cmd/malt-eval-rq3-hash-worker/selftest.go deleted file mode 100644 index 9d118a6..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-hash-worker/selftest.go +++ /dev/null @@ -1,468 +0,0 @@ -package main - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "slices" - "strings" - - "github.com/dewebprotocol/malt-client/internal/evaluation/e0selftest" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq3baseline" -) - -const hashAdapterSelfTestCorpusSchema = "malt-eval-rq3-hash-adapter-self-test/v3" - -var hashAdapterSelfTestProfile = e0selftest.Profile{ - ProfileID: "rq3-hash-adapters-positive-hostile-v3", - PositiveCases: []string{ - "execute-controlled-merkledag", - "execute-controlled-hamt", - "execute-git-first-parent-merkledag", - "execute-git-first-parent-hamt", - }, - HostileCases: []string{ - "reject-malformed-request", - "reject-tampered-payload", - "reject-stale-before-value", - "detect-commit-list-digest-mismatch", - "reject-unfinished-stream", - "reject-failed-stream-reuse", - "reject-active-stream-binding-reuse", - }, -} - -type hashAdapterSelfTestCorpus struct { - SchemaVersion string `json:"schema_version"` - PositiveCases []hashAdapterSelfTestPositiveCase `json:"positive_cases"` - HostileCases []hashAdapterSelfTestHostileCase `json:"hostile_cases"` -} - -type hashAdapterSelfTestPositiveCase struct { - ID string `json:"id"` - SourceKind string `json:"source_kind"` - Run rq3baseline.RunSpec `json:"run"` - TraceBinding *hashTraceBinding `json:"trace_binding,omitempty"` -} - -type hashTraceBinding struct { - TraceID string `json:"trace_id"` - CommitIDs []string `json:"commit_ids"` - CommitListSHA256 string `json:"commit_list_sha256"` -} - -type hashAdapterSelfTestHostileCase struct { - ID string `json:"id"` - Kind string `json:"kind"` -} - -type repeatedPathFlag []string - -func (f *repeatedPathFlag) String() string { return strings.Join(*f, ",") } -func (f *repeatedPathFlag) Set(value string) error { - *f = append(*f, value) - return nil -} - -func runHashAdapterSelfTest(ctx context.Context, corpusPath string, boundWorkloads, gitExecutables []string, output io.Writer) error { - if err := validateHashSelfTestInputs(corpusPath, boundWorkloads, gitExecutables); err != nil { - return err - } - var corpus hashAdapterSelfTestCorpus - if err := decodeHashSelfTestCorpus(corpusPath, &corpus); err != nil { - return err - } - if err := corpus.validate(); err != nil { - return err - } - results := make([]e0selftest.CaseResult, 0, len(corpus.PositiveCases)+len(corpus.HostileCases)) - for _, testCase := range corpus.PositiveCases { - if err := executeHashPositiveCase(ctx, testCase); err != nil { - return fmt.Errorf("hash adapter self-test positive case %q: %w", testCase.ID, err) - } - results = append(results, e0selftest.CaseResult{ID: testCase.ID, Passed: true}) - } - for _, testCase := range corpus.HostileCases { - if err := executeHashHostileCase(ctx, testCase, corpus.PositiveCases); err != nil { - return fmt.Errorf("hash adapter self-test hostile case %q: %w", testCase.ID, err) - } - results = append(results, e0selftest.CaseResult{ID: testCase.ID, Passed: true}) - } - receipt, err := e0selftest.Issue("rq3.hash-system-adapters", hashAdapterSelfTestProfile, results) - if err != nil { - return err - } - encoder := json.NewEncoder(output) - encoder.SetEscapeHTML(false) - return encoder.Encode(receipt) -} - -func (c hashAdapterSelfTestCorpus) validate() error { - if c.SchemaVersion != hashAdapterSelfTestCorpusSchema || len(c.PositiveCases) != len(hashAdapterSelfTestProfile.PositiveCases) || len(c.HostileCases) != len(hashAdapterSelfTestProfile.HostileCases) { - return errors.New("hash adapter self-test corpus has an invalid schema or case count") - } - for index, testCase := range c.PositiveCases { - if testCase.ID != hashAdapterSelfTestProfile.PositiveCases[index] { - return fmt.Errorf("hash adapter positive case %d has id %q", index, testCase.ID) - } - wantSystem := rq3baseline.SystemMerkleDAGUnixFS - wantSource := "controlled" - if testCase.ID == "execute-controlled-hamt" { - wantSystem = rq3baseline.SystemHAMTUnixFS - } - if testCase.ID == "execute-git-first-parent-merkledag" || testCase.ID == "execute-git-first-parent-hamt" { - wantSource = "git-first-parent" - } - if testCase.ID == "execute-git-first-parent-hamt" { - wantSystem = rq3baseline.SystemHAMTUnixFS - } - if testCase.SourceKind != wantSource || testCase.Run.System != wantSystem { - return fmt.Errorf("hash adapter positive case %q has a mismatched source/system", testCase.ID) - } - if testCase.SourceKind == "git-first-parent" { - if testCase.TraceBinding == nil || validateHashTraceBinding(testCase.Run, *testCase.TraceBinding) != nil { - return fmt.Errorf("hash adapter positive case %q has an invalid trace binding", testCase.ID) - } - } else if testCase.TraceBinding != nil { - return fmt.Errorf("controlled hash adapter case %q carries a trace binding", testCase.ID) - } - } - wantKinds := []string{"malformed-request", "tampered-payload", "stale-before-value", "commit-list-digest-mismatch", "unfinished-stream", "failed-stream-reuse", "active-stream-binding-reuse"} - for index, testCase := range c.HostileCases { - if testCase.ID != hashAdapterSelfTestProfile.HostileCases[index] || testCase.Kind != wantKinds[index] { - return fmt.Errorf("hash adapter hostile case %d does not match the compiled contract", index) - } - } - return nil -} - -func executeHashPositiveCase(ctx context.Context, testCase hashAdapterSelfTestPositiveCase) error { - handler := new(workerHandler) - if response, err := sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: testCase.ID + "-cap", Operation: rq3baseline.OperationCapabilities}); err != nil || !response.OK { - return fmt.Errorf("production hash capability preflight: response=%#v err=%w", response, err) - } - start := testCase.Run - start.Commits = []rq3baseline.Commit{} - response, err := sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: testCase.ID + "-start", Operation: rq3baseline.OperationStreamStart, Run: &start}) - if err != nil || !response.OK || response.Stream == nil || len(response.Stream.Records) != 1 { - return fmt.Errorf("production hash stream start: response=%#v err=%w", response, err) - } - records := append([]rq3baseline.CommitRecord{}, response.Stream.Records...) - for index, commit := range testCase.Run.Commits { - chunk := rq3baseline.RunSpec{System: testCase.Run.System, Layout: testCase.Run.Layout, Commits: []rq3baseline.Commit{commit}} - response, err = sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: fmt.Sprintf("%s-chunk-%d", testCase.ID, index), Operation: rq3baseline.OperationStreamChunk, Run: &chunk}) - if err != nil || !response.OK || response.Stream == nil || len(response.Stream.Records) != 1 { - return fmt.Errorf("production hash stream chunk %d: response=%#v err=%w", index, response, err) - } - records = append(records, response.Stream.Records...) - } - response, err = sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: testCase.ID + "-finish", Operation: rq3baseline.OperationStreamFinish}) - wantDigest, digestErr := hashCommitListDigest(append([]string{testCase.Run.Snapshot.CommitID}, func() []string { - values := make([]string, len(testCase.Run.Commits)) - for i := range testCase.Run.Commits { - values[i] = testCase.Run.Commits[i].CommitID - } - return values - }()...)) - if err != nil || digestErr != nil || !response.OK || response.Stream == nil || !response.Stream.Complete || response.Stream.CommitsApplied != uint32(len(records)) || response.Stream.CommitListSHA256 != wantDigest { - return fmt.Errorf("production hash stream finish: response=%#v err=%w", response, err) - } - for index, record := range records { - want := testCase.Run.Snapshot.CommitID - if index > 0 { - want = testCase.Run.Commits[index-1].CommitID - } - if record.CommitID != want || record.Root == "" { - return fmt.Errorf("hash result record %d does not bind commit %q", index, want) - } - if index > 0 && len(testCase.Run.Commits[index-1].Mutations) == 0 && - (record.Root != record.ParentRoot || record.LogicalObjectsChanged != 0 || record.LogicalBindingsChanged != 0 || record.LogicalPayloadBytes != 0 || record.AdapterPayloadInputBytes != 0 || - record.CAS.Total.AttemptedObjects != 0 || record.CAS.Total.AttemptedBytes != 0 || record.CAS.Total.NewlyPersistedObjects != 0 || record.CAS.Total.NewlyPersistedBytes != 0) { - return fmt.Errorf("hash no-op commit %q changed the root or emitted writes", want) - } - } - return nil -} - -func sendHashSelfTestRequest(ctx context.Context, handler *workerHandler, request rq3baseline.WorkerRequest) (rq3baseline.WorkerResponse, error) { - encoded, err := json.Marshal(request) - if err != nil { - return rq3baseline.WorkerResponse{}, err - } - return handler.handleLine(ctx, encoded), nil -} - -func executeHashHostileCase(ctx context.Context, testCase hashAdapterSelfTestHostileCase, positives []hashAdapterSelfTestPositiveCase) error { - switch testCase.Kind { - case "malformed-request": - handler := new(workerHandler) - if response, err := sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "cap", Operation: rq3baseline.OperationCapabilities}); err != nil || !response.OK { - return errors.New("production hash capability preflight failed") - } - response := handler.handleLine(ctx, []byte(`{"schema_version":"malt-rq3-hash-worker-request/v1","request_id":"malformed","operation":"stream-start","unknown":true}`)) - if response.OK || response.Error == nil || response.Error.Code != "invalid_request" { - return errors.New("production hash request decoder accepted an unknown field") - } - return nil - case "tampered-payload": - run, err := cloneHashRun(positives[0].Run) - if err != nil { - return err - } - run.Snapshot.Files[0].PayloadBase64 = "dGFtcGVyZWQ=" - run.Commits = []rq3baseline.Commit{} - handler := new(workerHandler) - _, _ = sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "cap", Operation: rq3baseline.OperationCapabilities}) - response, err := sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "tampered", Operation: rq3baseline.OperationStreamStart, Run: &run}) - if err != nil || response.OK || response.Error == nil || response.Error.Code != "invalid_or_failed_stream_start" { - return errors.New("production hash adapter accepted a payload whose bytes do not match its SHA-256 binding") - } - return nil - case "stale-before-value": - run, err := cloneHashRun(positives[0].Run) - if err != nil { - return err - } - run.Commits[0].Mutations[0].ExpectedOldSHA256 = strings.Repeat("0", 64) - start := run - start.Commits = []rq3baseline.Commit{} - handler := new(workerHandler) - _, _ = sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "cap", Operation: rq3baseline.OperationCapabilities}) - started, err := sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "start", Operation: rq3baseline.OperationStreamStart, Run: &start}) - if err != nil || !started.OK { - return errors.New("production hash hostile stream failed to start") - } - chunk := rq3baseline.RunSpec{System: run.System, Layout: run.Layout, Commits: []rq3baseline.Commit{run.Commits[0]}} - response, err := sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "stale-before", Operation: rq3baseline.OperationStreamChunk, Run: &chunk}) - if err != nil || response.OK || response.Error == nil || response.Error.Code != "invalid_or_failed_stream_chunk" { - return errors.New("production hash adapter accepted a stale expected-old value") - } - return nil - case "commit-list-digest-mismatch": - testCase := positives[2] - handler := new(workerHandler) - if response, err := sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "cap", Operation: rq3baseline.OperationCapabilities}); err != nil || !response.OK { - return errors.New("production hash capability preflight failed") - } - start := testCase.Run - start.Commits = []rq3baseline.Commit{} - if response, err := sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "start", Operation: rq3baseline.OperationStreamStart, Run: &start}); err != nil || !response.OK { - return errors.New("production hash digest-mismatch stream failed to start") - } - for index, commit := range testCase.Run.Commits { - chunk := rq3baseline.RunSpec{System: testCase.Run.System, Layout: testCase.Run.Layout, Commits: []rq3baseline.Commit{commit}} - if response, err := sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: fmt.Sprintf("chunk-%d", index), Operation: rq3baseline.OperationStreamChunk, Run: &chunk}); err != nil || !response.OK { - return errors.New("production hash digest-mismatch stream chunk failed") - } - } - response, err := sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "finish", Operation: rq3baseline.OperationStreamFinish}) - if err != nil || !response.OK || response.Stream == nil || response.Stream.CommitListSHA256 != testCase.TraceBinding.CommitListSHA256 || response.Stream.CommitListSHA256 == strings.Repeat("0", 64) { - return errors.New("production hash worker did not expose the actual commit-list digest for evaluator mismatch detection") - } - return nil - case "unfinished-stream": - run := positives[0].Run - start := run - start.Commits = []rq3baseline.Commit{} - input, err := encodeHashSelfTestRequests([]rq3baseline.WorkerRequest{ - {SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "cap", Operation: rq3baseline.OperationCapabilities}, - {SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "start", Operation: rq3baseline.OperationStreamStart, Run: &start}, - }) - if err != nil { - return err - } - if err := runWorker(ctx, bytes.NewReader(input), io.Discard); err == nil || !strings.Contains(err.Error(), "before stream-finish") { - return fmt.Errorf("production hash worker accepted EOF with an unfinished stream: %v", err) - } - return nil - case "failed-stream-reuse": - run, err := cloneHashRun(positives[0].Run) - if err != nil { - return err - } - run.Commits[0].Mutations[0].ExpectedOldSHA256 = strings.Repeat("0", 64) - start := run - start.Commits = []rq3baseline.Commit{} - handler := new(workerHandler) - _, _ = sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "cap", Operation: rq3baseline.OperationCapabilities}) - _, _ = sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "start", Operation: rq3baseline.OperationStreamStart, Run: &start}) - chunk := rq3baseline.RunSpec{System: run.System, Layout: run.Layout, Commits: []rq3baseline.Commit{run.Commits[0]}} - failed, _ := sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "fail", Operation: rq3baseline.OperationStreamChunk, Run: &chunk}) - reused, _ := sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "reuse", Operation: rq3baseline.OperationStreamFinish}) - if failed.OK || failed.Error == nil || reused.OK || reused.Error == nil || !strings.Contains(reused.Error.Message, "poisoned") { - return errors.New("production hash worker allowed reuse after a failed stream chunk") - } - return nil - case "active-stream-binding-reuse": - run := positives[0].Run - start := run - start.Commits = []rq3baseline.Commit{} - handler := new(workerHandler) - _, _ = sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "cap", Operation: rq3baseline.OperationCapabilities}) - _, _ = sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "start", Operation: rq3baseline.OperationStreamStart, Run: &start}) - wrongLayout := run.Layout - wrongLayout.FileLayout = "trickle" - chunk := rq3baseline.RunSpec{System: run.System, Layout: wrongLayout, Commits: []rq3baseline.Commit{run.Commits[0]}} - failed, _ := sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "wrong-binding", Operation: rq3baseline.OperationStreamChunk, Run: &chunk}) - reused, _ := sendHashSelfTestRequest(ctx, handler, rq3baseline.WorkerRequest{SchemaVersion: rq3baseline.WorkerRequestSchema, RequestID: "reuse", Operation: rq3baseline.OperationStreamFinish}) - if failed.OK || failed.Error == nil || reused.OK || reused.Error == nil || !strings.Contains(reused.Error.Message, "poisoned") { - return errors.New("production hash worker allowed reuse after an active-stream binding failure") - } - return nil - default: - return fmt.Errorf("unsupported hostile kind %q", testCase.Kind) - } -} - -func encodeHashSelfTestRequests(requests []rq3baseline.WorkerRequest) ([]byte, error) { - var input bytes.Buffer - encoder := json.NewEncoder(&input) - encoder.SetEscapeHTML(false) - for _, request := range requests { - if err := encoder.Encode(request); err != nil { - return nil, err - } - } - return input.Bytes(), nil -} - -func validateHashSelfTestInputs(corpusPath string, boundWorkloads, gitExecutables []string) error { - consumed := []string{corpusPath} - for _, path := range boundWorkloads { - if err := validateBoundWorkload(path); err != nil { - return fmt.Errorf("bound workload %q: %w", path, err) - } - consumed = append(consumed, path) - } - for _, path := range gitExecutables { - if err := validateBoundExecutable(path); err != nil { - return fmt.Errorf("Git executable %q: %w", path, err) - } - consumed = append(consumed, path) - } - return requireExactInvocationPaths(consumed) -} - -func validateBoundWorkload(path string) error { - data, err := os.ReadFile(path) - if err != nil { - return err - } - if len(data) == 0 || len(data) > maxJSONLRecordBytes { - return errors.New("workload is empty or exceeds the bounded size") - } - var identity struct { - SchemaVersion string `json:"schema_version"` - } - decoder := json.NewDecoder(bytes.NewReader(data)) - if err := decoder.Decode(&identity); err != nil { - return err - } - if identity.SchemaVersion != "malt-rq3-controlled-workload/v1" && identity.SchemaVersion != "malt-eval-git-trace-request/v1" { - return fmt.Errorf("unsupported production workload schema %q", identity.SchemaVersion) - } - return nil -} - -func validateBoundExecutable(path string) error { - if !filepath.IsAbs(path) || filepath.Clean(path) != path { - return errors.New("path must be absolute and clean") - } - info, err := os.Lstat(path) - if err != nil { - return err - } - if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() || info.Mode().Perm()&0o111 == 0 { - return errors.New("file must be a non-symlink executable") - } - return nil -} - -func requireExactInvocationPaths(consumed []string) error { - for index, path := range consumed { - if !filepath.IsAbs(path) || filepath.Clean(path) != path { - return fmt.Errorf("consumed path %d is not absolute and clean", index) - } - } - want, err := e0selftest.InputPaths() - if err != nil { - return err - } - slices.Sort(consumed) - if !slices.Equal(consumed, want) { - return fmt.Errorf("typed self-test flags consume %v, E0 invocation pins %v", consumed, want) - } - return nil -} - -func validateHashTraceBinding(run rq3baseline.RunSpec, binding hashTraceBinding) error { - commitIDs := make([]string, 0, len(run.Commits)+1) - commitIDs = append(commitIDs, run.Snapshot.CommitID) - for _, commit := range run.Commits { - commitIDs = append(commitIDs, commit.CommitID) - } - if binding.TraceID == "" || !slices.Equal(commitIDs, binding.CommitIDs) { - return errors.New("frozen run commit IDs do not equal the registered first-parent trace") - } - digest, err := hashCommitListDigest(commitIDs) - if err != nil { - return err - } - if digest != binding.CommitListSHA256 { - return errors.New("registered first-parent trace commit-list digest mismatch") - } - return nil -} - -func hashCommitListDigest(commitIDs []string) (string, error) { - encoded, err := json.Marshal(struct { - Commits []string `json:"commits"` - }{Commits: commitIDs}) - if err != nil { - return "", err - } - digest := sha256.Sum256(encoded) - return hex.EncodeToString(digest[:]), nil -} - -func cloneHashRun(run rq3baseline.RunSpec) (rq3baseline.RunSpec, error) { - encoded, err := json.Marshal(run) - if err != nil { - return rq3baseline.RunSpec{}, err - } - var clone rq3baseline.RunSpec - if err := json.Unmarshal(encoded, &clone); err != nil { - return rq3baseline.RunSpec{}, err - } - return clone, nil -} - -func decodeHashSelfTestCorpus(path string, target *hashAdapterSelfTestCorpus) error { - data, err := os.ReadFile(path) - if err != nil { - return fmt.Errorf("read hash adapter self-test corpus: %w", err) - } - if len(data) == 0 || len(data) > maxJSONLRecordBytes { - return errors.New("hash adapter self-test corpus is empty or exceeds the bounded size") - } - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.DisallowUnknownFields() - if err := decoder.Decode(target); err != nil { - return fmt.Errorf("decode strict hash adapter self-test corpus: %w", err) - } - var trailing json.RawMessage - if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { - if err == nil { - return errors.New("hash adapter self-test corpus contains a trailing JSON value") - } - return fmt.Errorf("decode hash adapter self-test corpus trailer: %w", err) - } - return nil -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/accounting.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/accounting.go deleted file mode 100644 index eebc981..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/accounting.go +++ /dev/null @@ -1,343 +0,0 @@ -package main - -import ( - "context" - "fmt" - "math" - "slices" - "time" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-client/transport" -) - -const ( - stageAttempted = "attempted" - stageCommitted = "committed" - - dispositionNew = "new" - dispositionSameValue = "same-value" - dispositionReplacement = "replacement" - dispositionDelete = "delete" - - casNotApplicable = "not-applicable" - casNew = "new" - casSameValue = "same-value" - - gatewayAccountingProfile = "gateway.authentication-write-accounting/0" - gatewayByteMethod = "durable-kv-key-plus-value-bytes/v2" - canonicalEmptySetupCause = "canonical-empty-setup:" - - // Keep each evaluator request bounded independently of the selected durable - // filesystem KV implementation. Keep the decoded - // CAS payload below that ceiling after keys and size sidecars are added. - maximumEvaluationGatewayCASBatchBytes = 8 << 20 -) - -var gatewayCategories = []string{"arctable-arcset-records", "arctable-lineage-metadata", "root-version-metadata"} - -type exactAccounting struct { - profile string - available bool - reason string - method string - digest string - categories []exactCategory -} - -type exactCategory struct { - category string - attemptedWrites uint64 - attemptedBytes uint64 - attemptedNewWrites uint64 - attemptedNewBytes uint64 - attemptedReplacementWrites uint64 - attemptedReplacementBytes uint64 - attemptedSameWrites uint64 - attemptedSameBytes uint64 - attemptedDeleteWrites uint64 - attemptedDeleteBytes uint64 - newlyPersistedWrites uint64 - grossNewBytes uint64 - newWrites uint64 - newBytes uint64 - replacedWrites uint64 - replacementNewBytes uint64 - replacementReclaimedBytes uint64 - sameWrites uint64 - deletedWrites uint64 - deletedReclaimedBytes uint64 - reclaimedBytes uint64 - netBytes int64 -} - -func accountingFromTransport(value gatewaytransport.WriteAccounting) exactAccounting { - result := exactAccounting{ - profile: value.Profile, available: value.Available, reason: value.UnavailableReason, - method: value.ByteMethod, digest: value.ObjectLedgerSHA256, - categories: make([]exactCategory, len(value.Categories)), - } - for index, category := range value.Categories { - result.categories[index] = exactCategory{ - category: category.Category, attemptedWrites: category.AttemptedWrites, attemptedBytes: category.AttemptedBytes, - attemptedNewWrites: category.AttemptedNewWrites, attemptedNewBytes: category.AttemptedNewBytes, - attemptedReplacementWrites: category.AttemptedReplacementWrites, attemptedReplacementBytes: category.AttemptedReplacementBytes, - attemptedSameWrites: category.AttemptedSameValueWrites, attemptedSameBytes: category.AttemptedSameValueBytes, - attemptedDeleteWrites: category.AttemptedDeleteWrites, attemptedDeleteBytes: category.AttemptedDeleteBytes, - newlyPersistedWrites: category.NewlyPersistedWrites, grossNewBytes: category.GrossNewBytes, - newWrites: category.NewWrites, newBytes: category.NewBytes, replacedWrites: category.ReplacedWrites, - replacementNewBytes: category.ReplacementNewBytes, replacementReclaimedBytes: category.ReplacementReclaimedBytes, - sameWrites: category.SameValueWrites, deletedWrites: category.DeletedWrites, - deletedReclaimedBytes: category.DeletedReclaimedBytes, reclaimedBytes: category.ReclaimedBytes, netBytes: category.NetBytes, - } - } - return result -} - -func (result *runResult) appendGatewayAccounting(commitID string, accounting exactAccounting) error { - if accounting.profile != gatewayAccountingProfile || accounting.method != gatewayByteMethod || !accounting.available || accounting.reason != "" || !canonicalSHA256(accounting.digest) || len(accounting.categories) != len(gatewayCategories) { - return fmt.Errorf("Gateway did not return the locked exact client-root accounting boundary") - } - for index, category := range accounting.categories { - if category.category != gatewayCategories[index] { - return fmt.Errorf("Gateway accounting category[%d] = %q", index, category.category) - } - if err := validateExactCategory(category); err != nil { - return fmt.Errorf("Gateway accounting %q: %w", category.category, err) - } - key := func(disposition string) string { - return "gateway-accounting/" + accounting.digest + "/" + category.category + "/" + disposition - } - appendAttempt := func(disposition string, count, bytes uint64) { - result.appendEvent(writeEvent{ - CommitID: commitID, Stage: stageAttempted, Category: category.category, - Cause: "gateway-authentication-object-ledger", Disposition: disposition, - ObjectKey: key(disposition), Count: count, Bytes: bytes, CASClassification: casNotApplicable, - }) - } - appendCommit := func(disposition string, count, gross, reclaimed uint64) error { - net, err := signedNet(gross, reclaimed) - if err != nil { - return err - } - flowBytes, err := sumUint64(gross, reclaimed) - if err != nil { - return err - } - result.appendEvent(writeEvent{ - CommitID: commitID, Stage: stageCommitted, Category: category.category, - Cause: "gateway-authentication-object-ledger", Disposition: disposition, - ObjectKey: key(disposition), Count: count, Bytes: flowBytes, GrossNewBytes: gross, - ReclaimedBytes: reclaimed, NetBytes: net, CASClassification: casNotApplicable, - }) - return nil - } - - if category.attemptedNewWrites > 0 { - appendAttempt(dispositionNew, category.attemptedNewWrites, category.attemptedNewBytes) - if err := appendCommit(dispositionNew, category.newWrites, category.newBytes, 0); err != nil { - return err - } - } - if category.attemptedReplacementWrites > 0 { - appendAttempt(dispositionReplacement, category.attemptedReplacementWrites, category.attemptedReplacementBytes) - if err := appendCommit(dispositionReplacement, category.replacedWrites, category.replacementNewBytes, category.replacementReclaimedBytes); err != nil { - return err - } - } - if category.attemptedSameWrites > 0 { - appendAttempt(dispositionSameValue, category.attemptedSameWrites, category.attemptedSameBytes) - } - if category.attemptedDeleteWrites > 0 { - appendAttempt(dispositionDelete, category.attemptedDeleteWrites, category.attemptedDeleteBytes) - if err := appendCommit(dispositionDelete, category.deletedWrites, 0, category.deletedReclaimedBytes); err != nil { - return err - } - } - } - return nil -} - -func validateExactCategory(value exactCategory) error { - attempts, err := sumUint64(value.attemptedNewWrites, value.attemptedReplacementWrites, value.attemptedSameWrites, value.attemptedDeleteWrites) - if err != nil { - return fmt.Errorf("attempted write count: %w", err) - } - attemptBytes, err := sumUint64(value.attemptedNewBytes, value.attemptedReplacementBytes, value.attemptedSameBytes, value.attemptedDeleteBytes) - if err != nil { - return fmt.Errorf("attempted byte count: %w", err) - } - persistedWrites, err := sumUint64(value.newWrites, value.replacedWrites) - if err != nil { - return fmt.Errorf("persisted write count: %w", err) - } - grossNewBytes, err := sumUint64(value.newBytes, value.replacementNewBytes) - if err != nil { - return fmt.Errorf("gross-new byte count: %w", err) - } - reclaimedBytes, err := sumUint64(value.replacementReclaimedBytes, value.deletedReclaimedBytes) - if err != nil { - return fmt.Errorf("reclaimed byte count: %w", err) - } - netBytes, err := signedNet(value.grossNewBytes, value.reclaimedBytes) - if err != nil { - return err - } - if value.attemptedWrites != attempts || value.attemptedBytes != attemptBytes || - value.newlyPersistedWrites != persistedWrites || value.grossNewBytes != grossNewBytes || - value.reclaimedBytes != reclaimedBytes || value.netBytes != netBytes { - return fmt.Errorf("aggregate counters are internally inconsistent") - } - if value.attemptedNewWrites != value.newWrites || - value.attemptedReplacementWrites != value.replacedWrites || - value.attemptedDeleteWrites != value.deletedWrites { - return fmt.Errorf("aggregate lifecycle cannot be represented without dropping write attempts") - } - if value.attemptedNewBytes != value.newBytes || value.attemptedReplacementBytes != value.replacementNewBytes { - return fmt.Errorf("aggregate attempted bytes do not bind their committed gross-new bytes") - } - if value.attemptedDeleteWrites > 0 && value.deletedReclaimedBytes == 0 { - return fmt.Errorf("delete lifecycle does not reclaim an old key/value") - } - return nil -} - -func (result *runResult) appendEvent(event writeEvent) { - event.Sequence = uint64(len(result.WriteEvents)) - result.WriteEvents = append(result.WriteEvents, event) -} - -// attributeCanonicalEmptySetup moves durable setup allocations into the first -// workload commit's ledger without moving setup work into that commit's timing -// interval. A distinct cause prefix makes the allocation separable in paper -// tables while preserving the exact CAS and Gateway object keys used for -// attempted/committed lifecycle pairing. -func attributeCanonicalEmptySetup(result, setup *runResult, firstCommitID string) { - if result == nil || setup == nil { - return - } - for _, event := range setup.WriteEvents { - event.CommitID = firstCommitID - event.Cause = canonicalEmptySetupCause + event.Cause - result.appendEvent(event) - } -} - -func uploadClassifiedBlocks(ctx context.Context, remote *transport.Client, commitID string, blocks []classifiedBlock, roles *blockRoleIndex, result *runResult) (time.Duration, error) { - var roleIndexElapsed time.Duration - for start := 0; start < len(blocks); { - end, err := nextClassifiedBlockBatchEnd(blocks, start, transport.MaxCASBatchBlocks, maximumEvaluationGatewayCASBatchBytes, transport.MaxCASBatchBytes) - if err != nil { - return roleIndexElapsed, err - } - batch := make([]transport.Block, end-start) - for index := range batch { - batch[index] = blocks[start+index].block - } - statuses, err := remote.PutBatch(ctx, batch) - if err != nil { - return roleIndexElapsed, fmt.Errorf("put exact CAS accounting batch: %w", err) - } - if len(statuses) != len(batch) { - return roleIndexElapsed, fmt.Errorf("Gateway returned %d CAS dispositions for %d blocks", len(statuses), len(batch)) - } - roleBatch := make([]blockRole, len(statuses)) - for index, status := range statuses { - roleBatch[index] = blockRole{key: status.CID.String(), category: blocks[start+index].category} - } - roleStarted := time.Now() - roleErr := roles.checkAndRecordBatch(roleBatch) - roleIndexElapsed += time.Since(roleStarted) - if roleErr != nil { - return roleIndexElapsed, roleErr - } - for index, status := range statuses { - classified := blocks[start+index] - key := status.CID.String() - length := uint64(len(classified.block.Data)) - objectKey := key + "/" + classified.suffix - collectAccounting := result.PassMode == "accounting" - switch status.Status { - case clientcas.PutStatusNewlyPersisted: - if collectAccounting { - result.appendEvent(writeEvent{ - CommitID: commitID, Stage: stageAttempted, Category: classified.category, Cause: classified.cause, - Disposition: dispositionNew, ObjectKey: objectKey, Bytes: length, CASClassification: casNew, - Count: 1, - }) - result.appendEvent(writeEvent{ - CommitID: commitID, Stage: stageCommitted, Category: classified.category, Cause: classified.cause, - Disposition: dispositionNew, ObjectKey: objectKey, Bytes: length, GrossNewBytes: length, - Count: 1, - NetBytes: int64(length), CASClassification: casNew, - }) - } - case clientcas.PutStatusAlreadyPresent, clientcas.PutStatusDuplicateInRequest: - if collectAccounting { - result.appendEvent(writeEvent{ - CommitID: commitID, Stage: stageAttempted, Category: classified.category, Cause: classified.cause, - Disposition: dispositionSameValue, ObjectKey: objectKey, Bytes: length, CASClassification: casSameValue, - Count: 1, - }) - } - default: - return roleIndexElapsed, fmt.Errorf("Gateway returned non-evaluation CAS disposition %q", status.Status) - } - } - start = end - } - return roleIndexElapsed, nil -} - -func nextClassifiedBlockBatchEnd(blocks []classifiedBlock, start, maximumBlocks, maximumBatchBytes, maximumSingleBlockBytes int) (int, error) { - if start < 0 || start >= len(blocks) { - return 0, fmt.Errorf("CAS batch start %d is outside %d blocks", start, len(blocks)) - } - if maximumBlocks <= 0 || maximumBatchBytes <= 0 || maximumSingleBlockBytes < maximumBatchBytes { - return 0, fmt.Errorf("CAS batch bounds must be positive and the single-block bound must cover the aggregate bound") - } - end, bytes := start, 0 - for end < len(blocks) && end-start < maximumBlocks { - length := len(blocks[end].block.Data) - if length > maximumSingleBlockBytes { - return 0, fmt.Errorf("CAS block exceeds transport batch byte bound") - } - // One legal transport block may exceed the conservative aggregate - // payload bound regardless of the selected persistence backend. - // Send that block alone and never combine it with neighboring blocks. - if end == start && length > maximumBatchBytes { - return start + 1, nil - } - if length > maximumBatchBytes || bytes > maximumBatchBytes-length { - break - } - bytes += length - end++ - } - return end, nil -} - -func signedNet(gross, reclaimed uint64) (int64, error) { - if gross > math.MaxInt64 || reclaimed > math.MaxInt64 { - return 0, fmt.Errorf("Gateway byte flow exceeds signed evaluator range") - } - return int64(gross) - int64(reclaimed), nil -} - -func sumUint64(values ...uint64) (uint64, error) { - var result uint64 - for _, value := range values { - if math.MaxUint64-result < value { - return 0, fmt.Errorf("aggregate overflows uint64") - } - result += value - } - return result, nil -} - -func canonicalSHA256(value string) bool { - return len(value) == 64 && slices.IndexFunc([]byte(value), func(character byte) bool { - return (character < '0' || character > '9') && (character < 'a' || character > 'f') - }) == -1 -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/direct.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/direct.go deleted file mode 100644 index 0679fd3..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/direct.go +++ /dev/null @@ -1,614 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "reflect" - "slices" - "strings" - "time" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq3baseline" - "github.com/dewebprotocol/malt-client/transport" - cid "github.com/ipfs/go-cid" -) - -type flatDirectStream struct { - worker *campaignWorker - base runSpec - source *rq3baseline.ValidationStreamSession - state map[string]logicalFile - roles *blockRoleIndex - root cid.Cid - oracle *flatRootOracle - nextOrder uint32 - commitIDs []string - commitManifest []string - payloads *logicalPayloadStore - failed bool -} - -func (w *campaignWorker) startFlatDirectStream(ctx context.Context, spec runSpec) (_ *flatDirectStream, _ *runResult, returnErr error) { - if !validRunCoordinate(spec.PassMode, spec.RunPhase, spec.ClusterID, spec.RunIndex) { - return nil, nil, fmt.Errorf("invalid raw RQ3 pass/run/cluster coordinate") - } - if err := validateWorkloadIdentity(spec.Workload); err != nil { - return nil, nil, err - } - if w.config.initialRoot != spec.InitialRoot || spec.InitialRoot != "" { - return nil, nil, fmt.Errorf("stream start requires a clean disposable Gateway and empty initial_root") - } - if spec.Commits == nil || len(spec.Commits) != 0 { - return nil, nil, fmt.Errorf("stream start requires an explicit empty commit chunk") - } - if spec.CommitManifest == nil || len(spec.CommitManifest) == 0 || spec.CommitManifest[0] != spec.Snapshot.CommitID { - return nil, nil, fmt.Errorf("stream start requires a complete commit manifest beginning at the snapshot") - } - if err := validateCommitManifest(spec.Workload, spec.CommitManifest); err != nil { - return nil, nil, err - } - if err := prevalidateMALTFlatRun(spec); err != nil { - return nil, nil, err - } - baselineSpec := rq3baseline.RunSpec{ - System: rq3baseline.SystemMerkleDAGUnixFS, - Layout: rq3baseline.LayoutSpec{Model: "unixfs", FileLayout: "balanced", DirectoryLayout: "basic", Chunking: rq3baseline.ChunkingSpec{Algorithm: "fixed", SizeBytes: int(spec.Workload.ChunkBytes)}, RawFileLeaf: boolPointer(true)}, - Snapshot: spec.Snapshot, Commits: []rq3baseline.Commit{}, - } - source, logical, err := rq3baseline.StartValidationStream(baselineSpec) - if err != nil { - return nil, nil, fmt.Errorf("frozen stream snapshot semantic prevalidation: %w", err) - } - if err := w.validateHealth(ctx); err != nil { - return nil, nil, errors.Join(err, retryMALTCleanup(source.Close)) - } - payloads, err := newLogicalPayloadStore() - if err != nil { - return nil, nil, errors.Join(err, retryMALTCleanup(source.Close)) - } - roles, err := newBlockRoleIndex() - if err != nil { - return nil, nil, errors.Join(err, retryMALTCleanup(source.Close), retryMALTCleanup(payloads.close)) - } - succeeded := false - defer func() { - if !succeeded { - returnErr = errors.Join(returnErr, retryMALTCleanup(source.Close), retryMALTCleanup(payloads.close), retryMALTCleanup(roles.close)) - } - }() - result := &runResult{SchemaVersion: runResultSchema, CapabilityID: capabilityID, System: systemMALTFlat, ResultScope: resultScopeStreamStart, PassMode: spec.PassMode, RunPhase: spec.RunPhase, ClusterID: spec.ClusterID, RunIndex: spec.RunIndex, Workload: spec.Workload, Commits: make([]commitRecord, 0, 1), WriteEvents: []writeEvent{}} - started := time.Now() - state, blocks, err := initialLogicalState(spec.Snapshot, spec.Workload.ChunkBytes, payloads) - if err != nil { - return nil, nil, err - } - if err := payloads.reconcile(state); err != nil { - return nil, nil, fmt.Errorf("reconcile streamed MALT-flat snapshot payloads: %w", err) - } - blocks = append(blocks, flatSentinelBlock()) - roleIndexElapsed, err := uploadClassifiedBlocks(ctx, w.remote, spec.Snapshot.CommitID, blocks, roles, result) - if err != nil { - return nil, nil, err - } - changes, err := directFlatSnapshotChanges(state) - if err != nil { - return nil, nil, err - } - oracleStarted := time.Now() - oracle, err := newFlatRootOracle(ctx, changes) - oracleElapsed := time.Since(oracleStarted) - if err != nil { - return nil, nil, err - } - applied, err := w.evaluation.ApplyEvaluationFlatPrefix(ctx, w.config.bootstrapAuthorizationToken, gatewaytransport.FlatPrefixMutation{OperationID: operationID(spec.Snapshot.CommitID, 0), Initial: true, Changes: changes}) - if err != nil { - return nil, nil, fmt.Errorf("apply streamed direct MALT-flat snapshot: %w", err) - } - if err := verifyFlatGatewayRoot("stream snapshot", oracle.root, applied.Root); err != nil { - return nil, nil, err - } - if spec.PassMode == "accounting" { - if err := result.appendGatewayAccounting(spec.Snapshot.CommitID, accountingFromTransport(applied.WriteAccounting)); err != nil { - return nil, nil, err - } - } - replayNS, err := evaluatorNanos(applied.ValidationAndStageNanos) - if err != nil { - return nil, nil, err - } - persistNS, err := evaluatorNanos(applied.PersistNanos) - if err != nil { - return nil, nil, err - } - result.Commits = append(result.Commits, commitRecord{Order: 0, CommitID: spec.Snapshot.CommitID, Root: applied.Root.String(), HistoryRootsRetained: 1, LogicalObjectsChanged: logical.LogicalObjectsChanged, LogicalBindingsChanged: logical.LogicalBindingsChanged, LogicalPayloadBytes: logical.AdapterPayloadInputBytes, AdapterPayloadInputBytes: logical.AdapterPayloadInputBytes, ClientComputeWallNS: durationNanos(time.Since(started) - oracleElapsed - roleIndexElapsed), GatewayApplyAndStageWallNS: replayNS, GatewayPersistWallNS: persistNS, OracleUnmeasured: true}) - base := spec - base.Snapshot.Files = nil - base.Commits = nil - base.CommitManifest = nil - succeeded = true - return &flatDirectStream{worker: w, base: base, source: source, state: state, roles: roles, root: applied.Root, oracle: oracle, nextOrder: 1, commitIDs: []string{spec.Snapshot.CommitID}, commitManifest: append([]string(nil), spec.CommitManifest...), payloads: payloads}, result, nil -} - -func (s *flatDirectStream) applyChunk(ctx context.Context, spec runSpec) (*runResult, error) { - if s == nil || s.worker == nil || s.source == nil || s.failed { - return nil, fmt.Errorf("direct MALT-flat stream is not active") - } - if spec.PassMode != s.base.PassMode || spec.RunPhase != s.base.RunPhase || spec.ClusterID != s.base.ClusterID || spec.RunIndex != s.base.RunIndex || spec.InitialRoot != "" || !reflect.DeepEqual(spec.Workload, s.base.Workload) || spec.Snapshot.CommitID != "" || spec.Snapshot.Files != nil || spec.Commits == nil || len(spec.Commits) == 0 || spec.CommitManifest != nil { - s.failed = true - return nil, fmt.Errorf("direct MALT-flat stream chunk does not bind the active run or carries a snapshot") - } - if int(s.nextOrder)+len(spec.Commits) > len(s.commitManifest) { - s.failed = true - return nil, fmt.Errorf("direct MALT-flat stream chunk exceeds the frozen commit manifest") - } - for index, commit := range spec.Commits { - if commit.CommitID != s.commitManifest[int(s.nextOrder)+index] { - s.failed = true - return nil, fmt.Errorf("direct MALT-flat stream chunk commit %d differs from the frozen commit manifest", index) - } - } - if err := prevalidateMALTFlatCommits(spec.Commits); err != nil { - s.failed = true - return nil, err - } - if err := prevalidateMALTFlatChunk(s.state, spec.Commits, s.base.Workload.ChunkBytes, s.base.Workload.Kind == "git-first-parent" || isControlledListHistory(s.base), s.payloads); err != nil { - s.failed = true - return nil, err - } - logicalRecords, err := s.source.ApplyChunk(spec.Commits) - if err != nil { - s.failed = true - return nil, fmt.Errorf("frozen stream chunk semantic prevalidation: %w", err) - } - result := &runResult{SchemaVersion: runResultSchema, CapabilityID: capabilityID, System: systemMALTFlat, ResultScope: resultScopeStreamChunk, PassMode: s.base.PassMode, RunPhase: s.base.RunPhase, ClusterID: s.base.ClusterID, RunIndex: s.base.RunIndex, Workload: s.base.Workload, Commits: make([]commitRecord, 0, len(spec.Commits)), WriteEvents: []writeEvent{}} - changedChunksOnly := s.base.Workload.Kind == "git-first-parent" || isControlledListHistory(s.base) - for index, commit := range spec.Commits { - s.failed = true - started := time.Now() - blocks, canonicalPayloadBytes, logicalChanges, err := applyFrozenCommit(s.state, commit, s.base.Workload.ChunkBytes, changedChunksOnly, s.payloads) - if err != nil { - s.failed = true - return nil, err - } - expectedRoot := s.oracle.root - var gatewayChanges []gatewaytransport.FlatPrefixChange - var oracleElapsed time.Duration - if len(logicalChanges) != 0 { - gatewayChanges, err = directFlatDeltaChanges(logicalChanges) - if err != nil { - return nil, err - } - oracleStarted := time.Now() - expectedRoot, err = s.oracle.apply(ctx, gatewayChanges) - oracleElapsed = time.Since(oracleStarted) - if err != nil { - return nil, err - } - } - if err := verifyFlatGatewayRoot("stream pre-commit", expectedRoot, s.oracle.root); err != nil { - return nil, err - } - roleIndexElapsed, err := uploadClassifiedBlocks(ctx, s.worker.remote, commit.CommitID, blocks, s.roles, result) - if err != nil { - s.failed = true - return nil, err - } - nextRoot := s.root - var replayNS, persistNS int64 - if len(logicalChanges) != 0 { - next, err := s.worker.evaluation.ApplyEvaluationFlatPrefix(ctx, s.worker.config.bootstrapAuthorizationToken, gatewaytransport.FlatPrefixMutation{OperationID: operationID(commit.CommitID, s.nextOrder), BaseRoot: s.root, Changes: gatewayChanges}) - if err != nil { - s.failed = true - return nil, fmt.Errorf("apply streamed direct MALT-flat commit %q: %w", commit.CommitID, err) - } - if err := verifyFlatGatewayRoot("stream commit "+commit.CommitID, expectedRoot, next.Root); err != nil { - return nil, err - } - if s.base.PassMode == "accounting" { - if err := result.appendGatewayAccounting(commit.CommitID, accountingFromTransport(next.WriteAccounting)); err != nil { - s.failed = true - return nil, err - } - } - replayNS, err = evaluatorNanos(next.ValidationAndStageNanos) - if err != nil { - return nil, err - } - persistNS, err = evaluatorNanos(next.PersistNanos) - if err != nil { - return nil, err - } - nextRoot = next.Root - } else if err := verifyFlatGatewayRoot("stream unchanged commit "+commit.CommitID, expectedRoot, nextRoot); err != nil { - return nil, err - } - logical := logicalRecords[index] - result.Commits = append(result.Commits, commitRecord{Order: s.nextOrder, CommitID: commit.CommitID, ParentRoot: s.root.String(), Root: nextRoot.String(), HistoryRootsRetained: s.nextOrder + 1, LogicalObjectsChanged: logical.LogicalObjectsChanged, LogicalBindingsChanged: logical.LogicalBindingsChanged, LogicalPayloadBytes: canonicalPayloadBytes, AdapterPayloadInputBytes: logical.AdapterPayloadInputBytes, ClientComputeWallNS: durationNanos(time.Since(started) - oracleElapsed - roleIndexElapsed), GatewayApplyAndStageWallNS: replayNS, GatewayPersistWallNS: persistNS, OracleUnmeasured: true}) - s.root = nextRoot - s.nextOrder++ - s.commitIDs = append(s.commitIDs, commit.CommitID) - if err := s.payloads.reconcile(s.state); err != nil { - return nil, fmt.Errorf("reconcile streamed MALT-flat commit %q payloads: %w", commit.CommitID, err) - } - s.failed = false - } - return result, nil -} - -func (s *flatDirectStream) finish(ctx context.Context) (streamStatus, error) { - if s == nil || s.worker == nil || s.source == nil || s.failed { - return streamStatus{}, fmt.Errorf("direct MALT-flat stream is not active") - } - if int(s.nextOrder) != len(s.commitManifest) || !slices.Equal(s.commitIDs, s.commitManifest) { - s.failed = true - return streamStatus{}, fmt.Errorf("direct MALT-flat stream ended before the frozen commit manifest was exhausted") - } - oracle, err := fullFlatRoot(ctx, s.state) - if err != nil { - s.failed = true - return streamStatus{}, fmt.Errorf("build final streamed MALT-flat independent full oracle: %w", err) - } - if !oracle.Equals(s.root) { - s.failed = true - return streamStatus{}, fmt.Errorf("final streamed MALT-flat root differs from independent full oracle") - } - if s.oracle == nil || !s.oracle.root.Equals(s.root) { - s.failed = true - return streamStatus{}, fmt.Errorf("final streamed MALT-flat root differs from incremental per-commit oracle") - } - status := streamStatus{Root: s.root.String(), CommitsApplied: s.nextOrder, Complete: true} - if err := s.close(); err != nil { - return streamStatus{}, err - } - return status, nil -} - -func (s *flatDirectStream) close() error { - if s == nil { - return nil - } - var sourceErr error - if s.source != nil { - sourceErr = s.source.Close() - if sourceErr == nil { - s.source = nil - } - } - var payloadErr error - if s.payloads != nil { - payloadErr = s.payloads.close() - if payloadErr == nil { - s.payloads = nil - } - } - var rolesErr error - if s.roles != nil { - rolesErr = s.roles.close() - if rolesErr == nil { - s.roles = nil - } - } - s.failed = true - return errors.Join(sourceErr, payloadErr, rolesErr) -} - -func (w *campaignWorker) runFlatDirect(ctx context.Context, spec runSpec) (_ *runResult, returnErr error) { - payloads, err := newLogicalPayloadStore() - if err != nil { - return nil, err - } - roles, err := newBlockRoleIndex() - if err != nil { - return nil, errors.Join(err, retryMALTCleanup(payloads.close)) - } - defer func() { - returnErr = errors.Join(returnErr, retryMALTCleanup(payloads.close), retryMALTCleanup(roles.close)) - }() - baselineSpec := rq3baseline.RunSpec{ - System: rq3baseline.SystemMerkleDAGUnixFS, - Layout: rq3baseline.LayoutSpec{ - Model: "unixfs", FileLayout: "balanced", DirectoryLayout: "basic", - Chunking: rq3baseline.ChunkingSpec{Algorithm: "fixed", SizeBytes: int(spec.Workload.ChunkBytes)}, - RawFileLeaf: boolPointer(true), - }, - Snapshot: spec.Snapshot, Commits: spec.Commits, - } - source, err := rq3baseline.ValidateAndAccountSource(baselineSpec) - if err != nil { - return nil, fmt.Errorf("frozen workload semantic prevalidation: %w", err) - } - result := &runResult{ - SchemaVersion: runResultSchema, CapabilityID: capabilityID, System: systemMALTFlat, - ResultScope: resultScopeComplete, - PassMode: spec.PassMode, RunPhase: spec.RunPhase, ClusterID: spec.ClusterID, RunIndex: spec.RunIndex, - Workload: spec.Workload, Commits: make([]commitRecord, 0, len(source)), WriteEvents: []writeEvent{}, - } - started := time.Now() - state, blocks, err := initialLogicalState(spec.Snapshot, spec.Workload.ChunkBytes, payloads) - if err != nil { - return nil, err - } - if err := payloads.reconcile(state); err != nil { - return nil, fmt.Errorf("reconcile direct MALT-flat snapshot payloads: %w", err) - } - blocks = append(blocks, flatSentinelBlock()) - roleIndexElapsed, err := uploadClassifiedBlocks(ctx, w.remote, spec.Snapshot.CommitID, blocks, roles, result) - if err != nil { - return nil, err - } - changes, err := directFlatSnapshotChanges(state) - if err != nil { - return nil, err - } - oracleStarted := time.Now() - rootOracle, err := newFlatRootOracle(ctx, changes) - oracleElapsed := time.Since(oracleStarted) - if err != nil { - return nil, err - } - applied, err := w.evaluation.ApplyEvaluationFlatPrefix(ctx, w.config.bootstrapAuthorizationToken, gatewaytransport.FlatPrefixMutation{ - OperationID: operationID(spec.Snapshot.CommitID, 0), Initial: true, Changes: changes, - }) - if err != nil { - return nil, fmt.Errorf("apply direct MALT-flat snapshot: %w", err) - } - if err := verifyFlatGatewayRoot("snapshot", rootOracle.root, applied.Root); err != nil { - return nil, err - } - clientNS := durationNanos(time.Since(started) - oracleElapsed - roleIndexElapsed) - if spec.PassMode == "accounting" { - if err := result.appendGatewayAccounting(spec.Snapshot.CommitID, accountingFromTransport(applied.WriteAccounting)); err != nil { - return nil, err - } - } - replayNS, err := evaluatorNanos(applied.ValidationAndStageNanos) - if err != nil { - return nil, err - } - persistNS, err := evaluatorNanos(applied.PersistNanos) - if err != nil { - return nil, err - } - result.Commits = append(result.Commits, commitRecord{ - Order: 0, CommitID: spec.Snapshot.CommitID, Root: applied.Root.String(), HistoryRootsRetained: 1, - LogicalObjectsChanged: source[0].LogicalObjectsChanged, LogicalBindingsChanged: source[0].LogicalBindingsChanged, - LogicalPayloadBytes: source[0].AdapterPayloadInputBytes, AdapterPayloadInputBytes: source[0].AdapterPayloadInputBytes, - ClientComputeWallNS: clientNS, GatewayApplyAndStageWallNS: replayNS, - GatewayPersistWallNS: persistNS, OracleUnmeasured: true, - }) - root := applied.Root - changedChunksOnly := spec.Workload.Kind == "git-first-parent" || isControlledListHistory(spec) - for index, commit := range spec.Commits { - started = time.Now() - blocks, canonicalPayloadBytes, logicalChanges, err := applyFrozenCommit(state, commit, spec.Workload.ChunkBytes, changedChunksOnly, payloads) - if err != nil { - return nil, err - } - expectedRoot := rootOracle.root - var gatewayChanges []gatewaytransport.FlatPrefixChange - oracleElapsed = 0 - if len(logicalChanges) != 0 { - gatewayChanges, err = directFlatDeltaChanges(logicalChanges) - if err != nil { - return nil, err - } - oracleStarted = time.Now() - expectedRoot, err = rootOracle.apply(ctx, gatewayChanges) - oracleElapsed = time.Since(oracleStarted) - if err != nil { - return nil, err - } - } - roleIndexElapsed, err := uploadClassifiedBlocks(ctx, w.remote, commit.CommitID, blocks, roles, result) - if err != nil { - return nil, err - } - nextRoot := root - replayNS, persistNS = 0, 0 - if len(logicalChanges) != 0 { - next, err := w.evaluation.ApplyEvaluationFlatPrefix(ctx, w.config.bootstrapAuthorizationToken, gatewaytransport.FlatPrefixMutation{ - OperationID: operationID(commit.CommitID, uint32(index+1)), BaseRoot: root, Changes: gatewayChanges, - }) - if err != nil { - return nil, fmt.Errorf("apply direct MALT-flat commit %q: %w", commit.CommitID, err) - } - if err := verifyFlatGatewayRoot("commit "+commit.CommitID, expectedRoot, next.Root); err != nil { - return nil, err - } - if spec.PassMode == "accounting" { - if err := result.appendGatewayAccounting(commit.CommitID, accountingFromTransport(next.WriteAccounting)); err != nil { - return nil, err - } - } - replayNS, err = evaluatorNanos(next.ValidationAndStageNanos) - if err != nil { - return nil, err - } - persistNS, err = evaluatorNanos(next.PersistNanos) - if err != nil { - return nil, err - } - nextRoot = next.Root - } else if err := verifyFlatGatewayRoot("unchanged commit "+commit.CommitID, expectedRoot, nextRoot); err != nil { - return nil, err - } - clientNS = durationNanos(time.Since(started) - oracleElapsed - roleIndexElapsed) - logical := source[index+1] - result.Commits = append(result.Commits, commitRecord{ - Order: uint32(index + 1), CommitID: commit.CommitID, ParentRoot: root.String(), Root: nextRoot.String(), - HistoryRootsRetained: uint32(index + 2), LogicalObjectsChanged: logical.LogicalObjectsChanged, - LogicalBindingsChanged: logical.LogicalBindingsChanged, LogicalPayloadBytes: canonicalPayloadBytes, - AdapterPayloadInputBytes: logical.AdapterPayloadInputBytes, ClientComputeWallNS: clientNS, - GatewayApplyAndStageWallNS: replayNS, GatewayPersistWallNS: persistNS, - OracleUnmeasured: true, - }) - root = nextRoot - if err := payloads.reconcile(state); err != nil { - return nil, fmt.Errorf("reconcile direct MALT-flat commit %q payloads: %w", commit.CommitID, err) - } - } - oracle, err := fullFlatRoot(ctx, state) - if err != nil { - return nil, fmt.Errorf("build final direct MALT-flat independent full oracle: %w", err) - } - if !oracle.Equals(root) { - return nil, fmt.Errorf("final direct MALT-flat root differs from independent full oracle") - } - if !rootOracle.root.Equals(root) { - return nil, fmt.Errorf("final direct MALT-flat root differs from incremental per-commit oracle") - } - if spec.PassMode == "timing" && len(result.WriteEvents) != 0 { - return nil, fmt.Errorf("timing pass retained byte-accounting events") - } - return result, nil -} - -func prevalidateMALTFlatRun(spec runSpec) error { - if len(spec.Snapshot.Files) > maximumMALTSnapshotFiles { - return fmt.Errorf("MALT-flat snapshot has %d files; maximum is %d", len(spec.Snapshot.Files), maximumMALTSnapshotFiles) - } - for index, file := range spec.Snapshot.Files { - payload, err := decodeFrozenPayload(file.PayloadBase64) - if err != nil { - return fmt.Errorf("MALT-flat snapshot file[%d]: %w", index, err) - } - if len(payload) > maximumMALTWholeFileBytes { - return fmt.Errorf("MALT-flat snapshot file[%d] exceeds %d-byte whole-file CAS limit", index, maximumMALTWholeFileBytes) - } - } - return prevalidateMALTFlatCommits(spec.Commits) -} - -func prevalidateMALTFlatCommits(commits []rq3baseline.Commit) error { - total := 0 - for commitIndex, commit := range commits { - total += len(commit.Mutations) - if total > maximumMALTChunkMutations { - return fmt.Errorf("MALT-flat chunk has more than %d mutations", maximumMALTChunkMutations) - } - for mutationIndex, mutation := range commit.Mutations { - if mutation.PayloadBase64 == "" { - continue - } - payload, err := decodeFrozenPayload(mutation.PayloadBase64) - if err != nil { - return fmt.Errorf("MALT-flat commit[%d] mutation[%d]: %w", commitIndex, mutationIndex, err) - } - if len(payload) > maximumMALTWholeFileBytes { - return fmt.Errorf("MALT-flat commit[%d] mutation[%d] exceeds %d-byte whole-file CAS limit", commitIndex, mutationIndex, maximumMALTWholeFileBytes) - } - } - } - return nil -} - -func prevalidateMALTFlatChunk(state map[string]logicalFile, commits []rq3baseline.Commit, chunkBytes uint64, changedChunksOnly bool, payloads *logicalPayloadStore) (returnErr error) { - overlay, err := newLogicalPayloadOverlay(payloads) - if err != nil { - return fmt.Errorf("create MALT-flat chunk prevalidation payload overlay: %w", err) - } - defer func() { returnErr = errors.Join(returnErr, retryMALTCleanup(overlay.close)) }() - projected := make(map[string]logicalFile, len(state)) - for path, file := range state { - projected[path] = file - } - for index, commit := range commits { - _, _, changes, err := applyFrozenCommit(projected, commit, chunkBytes, changedChunksOnly, overlay) - if err != nil { - return fmt.Errorf("prevalidate MALT-flat commit[%d]: %w", index, err) - } - if len(changes) == 0 { - continue - } - if _, err := directFlatDeltaChanges(changes); err != nil { - return fmt.Errorf("prevalidate MALT-flat commit[%d] changes: %w", index, err) - } - } - return nil -} - -func directFlatSnapshotChanges(state map[string]logicalFile) ([]gatewaytransport.FlatPrefixChange, error) { - if len(state) > maximumMALTSnapshotFiles { - return nil, fmt.Errorf("flat snapshot exceeds file bound") - } - return flatSnapshotChanges(state) -} - -// flatSnapshotChanges projects every live file into the complete flat state. -// Gateway request limits belong to directFlatSnapshotChanges: a history can -// grow beyond its initial request size and still needs a complete final oracle. -func flatSnapshotChanges(state map[string]logicalFile) ([]gatewaytransport.FlatPrefixChange, error) { - sentinel, err := flatSentinelEntry() - if err != nil { - return nil, err - } - changes := make([]gatewaytransport.FlatPrefixChange, 0, 1+2*len(state)) - changes = append(changes, gatewaytransport.FlatPrefixChange{Label: sentinel.Label, After: sentinel.Target}) - for path, file := range state { - for _, mode := range []bool{false, true} { - target, err := directFlatTarget(&file, mode) - if err != nil { - return nil, err - } - changes = append(changes, gatewaytransport.FlatPrefixChange{Label: flatInput(path, mode), After: target}) - } - } - slices.SortFunc(changes, compareFlatChanges) - return changes, nil -} - -func directFlatDeltaChanges(changes []fileChange) ([]gatewaytransport.FlatPrefixChange, error) { - result := make([]gatewaytransport.FlatPrefixChange, 0, 2*len(changes)) - for _, change := range changes { - for _, mode := range []bool{false, true} { - coordinate := flatInput(change.path, mode) - before, err := directFlatTarget(change.before, mode) - if err != nil { - return nil, err - } - after, err := directFlatTarget(change.after, mode) - if err != nil { - return nil, err - } - if before.Defined() && after.Defined() && before.Equals(after) { - continue - } - result = append(result, gatewaytransport.FlatPrefixChange{ - Label: coordinate, Before: before, After: after, - }) - } - } - slices.SortFunc(result, compareFlatChanges) - if len(result) == 0 { - return nil, fmt.Errorf("direct flat delta contains no changed coordinates") - } - if len(result) > maximumGatewayFlatPrefixChanges { - return nil, fmt.Errorf("direct flat delta exceeds %d Gateway changes", maximumGatewayFlatPrefixChanges) - } - return result, nil -} - -func directFlatTarget(file *logicalFile, mode bool) (cid.Cid, error) { - if file == nil { - return cid.Undef, nil - } - if !mode && file.payload.Defined() { - return file.payload, nil - } - block := transport.Block{Codec: cid.Raw, Data: file.data} - if mode { - sidecar, err := modeCASBlock(file.mode) - if err != nil { - return cid.Undef, err - } - block = sidecar.block - } - return clientcas.CIDForBlock(block) -} - -func compareFlatChanges(left, right gatewaytransport.FlatPrefixChange) int { - return strings.Compare(string(left.Label), string(right.Label)) -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/flat.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/flat.go deleted file mode 100644 index 46b1fc9..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/flat.go +++ /dev/null @@ -1,109 +0,0 @@ -package main - -import ( - "context" - "fmt" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-client/transport" - "github.com/dewebprotocol/malt-core/auth/commitment/kzg" - "github.com/dewebprotocol/malt-core/derivation" - "github.com/dewebprotocol/malt-core/engine" - "github.com/dewebprotocol/malt-core/maltcid" - "github.com/dewebprotocol/malt-core/sdk/authentication" - cid "github.com/ipfs/go-cid" -) - -const ( - flatSentinelCause = "canonical-empty-setup:flat-layout-sentinel" - flatSentinelJSON = `{"schema_version":"malt-eval-rq3-flat-layout/v1"}` -) - -// The unmeasured oracle computes expected Roots before each Gateway request. -// Core Writers own a closed immutable node DAG: retaining only the newest -// Writer shares unchanged descendants without retaining obsolete ancestors. -type flatRootOracle struct { - writer *authentication.Writer - root cid.Cid -} - -func newFlatRootOracle(ctx context.Context, initial []gatewaytransport.FlatPrefixChange) (*flatRootOracle, error) { - if len(initial) == 0 { - return nil, fmt.Errorf("flat oracle requires an initial state") - } - scheme, err := kzg.NewScheme() - if err != nil { - return nil, err - } - profiles := engine.NewRegistry() - if err := profiles.Register(scheme); err != nil { - return nil, err - } - state := engine.State{Descriptor: maltcid.RootDescriptor{Layout: maltcid.Prefix, DerivationProfile: uint8(derivation.SHA256), Profile: maltcid.KZG4096}, Entries: make([]engine.Entry, len(initial))} - for i, change := range initial { - if len(change.Label) == 0 || change.Before.Defined() || !change.After.Defined() { - return nil, fmt.Errorf("invalid flat oracle initial binding %d", i) - } - state.Entries[i] = engine.Entry{Label: change.Label, Target: change.After} - } - writer, err := authentication.BuildWriter(ctx, engine.New(profiles), state) - if err != nil { - return nil, err - } - return &flatRootOracle{writer: writer, root: writer.Root()}, nil -} -func (o *flatRootOracle) apply(ctx context.Context, changes []gatewaytransport.FlatPrefixChange) (cid.Cid, error) { - if o == nil || o.writer == nil { - return cid.Undef, fmt.Errorf("flat oracle is not initialized") - } - delta := authentication.Delta{Changes: make([]engine.Change, len(changes))} - for i, change := range changes { - if len(change.Label) == 0 { - return cid.Undef, fmt.Errorf("flat change requires an explicit label") - } - delta.Changes[i] = engine.Change{Label: change.Label, Before: change.Before, After: change.After} - } - writer, err := o.writer.Apply(ctx, delta) - if err != nil { - return cid.Undef, err - } - o.writer, o.root = writer, writer.Root() - return o.root, nil -} - -// fullFlatRoot reconstructs the complete post-image from live source files, -// without using the mutation deltas or any Gateway response/state. Its local -// reconstruction is independent of the single-request Gateway snapshot limit. -func fullFlatRoot(ctx context.Context, files map[string]logicalFile) (cid.Cid, error) { - initial, err := flatSnapshotChanges(files) - if err != nil { - return cid.Undef, err - } - oracle, err := newFlatRootOracle(ctx, initial) - if err != nil { - return cid.Undef, err - } - return oracle.root, nil -} -func verifyFlatGatewayRoot(operation string, expected, observed cid.Cid) error { - if !expected.Defined() || !observed.Defined() || !expected.Equals(observed) { - return fmt.Errorf("%s Gateway Root differs from independent per-commit flat oracle", operation) - } - return nil -} -func flatSentinelBlock() classifiedBlock { - return classifiedBlock{block: transport.Block{Codec: cid.DagJSON, Data: []byte(flatSentinelJSON)}, category: categoryCASMetadata, cause: flatSentinelCause, suffix: "flat-layout-sentinel"} -} -func flatSentinelEntry() (engine.Entry, error) { - block := flatSentinelBlock() - key, err := clientcas.CIDForBlock(block.block) - return engine.Entry{Label: []byte("@malt-eval/layout"), Target: key}, err -} -func flatInput(filePath string, mode bool) []byte { - prefix := "rq3/files/" - if mode { - prefix = "rq3/modes/" - } - return []byte(prefix + filePath) -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/flat_oracle_test.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/flat_oracle_test.go deleted file mode 100644 index ff91973..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/flat_oracle_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -import ( - "fmt" - "testing" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - cid "github.com/ipfs/go-cid" -) - -func TestFullFlatRootAcceptsStateBeyondSnapshotRequestLimit(t *testing.T) { - payload, err := clientcas.CIDForBlock(clientcas.Block{Codec: cid.Raw, Data: []byte("retained payload")}) - if err != nil { - t.Fatal(err) - } - // Model live disk-backed source metadata: the full oracle must use each - // retained payload CID, without loading file bodies or using Gateway state. - file := logicalFile{payload: payload, mode: 0o644} - files := make(map[string]logicalFile, maximumMALTSnapshotFiles+1) - var lastPath string - for i := 0; i <= maximumMALTSnapshotFiles; i++ { - lastPath = fmt.Sprintf("src/file-%06d.txt", i) - files[lastPath] = file - } - if _, err := directFlatSnapshotChanges(files); err == nil { - t.Fatal("oversized Gateway snapshot request was accepted") - } - changes, err := flatSnapshotChanges(files) - if err != nil { - t.Fatal(err) - } - if got, want := len(changes), 1+2*len(files); got != want { - t.Fatalf("full projection has %d bindings; want %d", got, want) - } - full, err := fullFlatRoot(t.Context(), files) - if err != nil { - t.Fatalf("full oracle for %d live files: %v", len(files), err) - } - if !full.Defined() { - t.Fatal("full oracle returned an undefined Root") - } - - // The exact request boundary still accepts a complete initial snapshot. - // Incremental/full Root agreement is covered by the small-state oracle test; - // avoid a second large KZG construction in the default test suite. - delete(files, lastPath) - initial, err := directFlatSnapshotChanges(files) - if err != nil { - t.Fatal(err) - } - if got, want := len(initial), 1+2*len(files); got != want { - t.Fatalf("request projection has %d bindings; want %d", got, want) - } -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/main.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/main.go deleted file mode 100644 index 456b8ef..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/main.go +++ /dev/null @@ -1,315 +0,0 @@ -// Command malt-eval-rq3-malt-worker executes the paper RQ3 MALT-flat-KZG -// path-to-blob write path against one disposable, token-bound Gateway. -package main - -import ( - "bufio" - "bytes" - "context" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "os" - "os/signal" - "strings" - "time" - "unicode/utf8" -) - -func main() { - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) - defer stop() - if err := run(ctx, os.Args[1:], os.Stdin, os.Stdout); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func run(ctx context.Context, arguments []string, input io.Reader, output io.Writer) error { - if hasMALTSelfTestFlag(arguments) { - config, err := parseMALTSelfTestFlags(arguments) - if err != nil { - return err - } - return runMALTAdapterSelfTest(ctx, config, output) - } - config, err := parseFlags(arguments) - if err != nil { - return err - } - worker, err := newCampaignWorker(config) - if err != nil { - return err - } - return runWorker(ctx, input, output, worker) -} - -func parseFlags(arguments []string) (workerConfig, error) { - flags := flag.NewFlagSet("malt-eval-rq3-malt-worker", flag.ContinueOnError) - flags.SetOutput(io.Discard) - baseURL := flags.String("gateway-base-url", "", "disposable Gateway base URL") - token := flags.String("gateway-instance-token", "", "disposable Gateway instance token") - bootstrapAuthorizationToken := flags.String("gateway-bootstrap-authorization-token", "", "secret disposable Gateway bootstrap capability") - timeout := flags.Duration("request-timeout", 0, "per-request Gateway timeout") - initialRoot := flags.String("initial-root", "", "externally supplied initial root (unsupported for fair snapshot accounting)") - if err := flags.Parse(arguments); err != nil { - return workerConfig{}, err - } - if flags.NArg() != 0 || strings.TrimSpace(*baseURL) != *baseURL || *baseURL == "" || - !canonicalSHA256(*token) || !canonicalSHA256(*bootstrapAuthorizationToken) || *bootstrapAuthorizationToken == *token || - *timeout <= 0 || *timeout > 24*time.Hour || strings.TrimSpace(*initialRoot) != *initialRoot { - return workerConfig{}, fmt.Errorf("required flags are -gateway-base-url, canonical and distinct -gateway-instance-token/-gateway-bootstrap-authorization-token, and positive -request-timeout") - } - return workerConfig{ - gatewayBaseURL: *baseURL, instanceToken: *token, bootstrapAuthorizationToken: *bootstrapAuthorizationToken, - requestTimeout: *timeout, initialRoot: *initialRoot, - }, nil -} - -func runWorker(ctx context.Context, input io.Reader, output io.Writer, worker *campaignWorker) (returnErr error) { - if worker == nil { - return fmt.Errorf("RQ3 MALT worker is nil") - } - scanner := bufio.NewScanner(input) - scanner.Buffer(make([]byte, 64<<10), maxWorkerLineBytes) - encoder := json.NewEncoder(output) - encoder.SetEscapeHTML(false) - requests := 0 - capabilityPassed := false - var stream *flatDirectStream - closeStream := func() error { - if stream == nil { - return nil - } - err := stream.close() - if err == nil { - stream = nil - } - return err - } - defer func() { returnErr = errors.Join(returnErr, closeStream()) }() - terminal := false - for scanner.Scan() { - requests++ - if requests > maxWorkerRequests { - return fmt.Errorf("RQ3 MALT worker exceeds %d-request process limit", maxWorkerRequests) - } - if err := ctx.Err(); err != nil { - return err - } - request, err := decodeWorkerRequest(scanner.Bytes()) - var response workerResponse - if err != nil { - if stream != nil { - err = errors.Join(err, closeStream()) - terminal = true - } - response = failedResponse("", "invalid_request", err) - } else if err := validateRequestEnvelope(request, requests, capabilityPassed); err != nil { - if stream != nil { - err = errors.Join(err, closeStream()) - terminal = true - } - response = failedResponse(request.RequestID, "invalid_request", err) - } else if request.Operation == "capabilities" { - capability := supportedCapability() - if err := worker.validateHealth(ctx); err != nil { - capability.Supported = false - capability.MissingCategories = []string{"exact-gateway-boundary"} - capability.MissingMetrics = []string{"gateway-health-preflight"} - response = failedResponse(request.RequestID, "capability_unavailable", err) - response.Capability = &capability - } else { - capabilityPassed = true - response = workerResponse{ - SchemaVersion: workerResponseSchema, RequestID: request.RequestID, OK: true, Capability: &capability, - } - } - } else if terminal { - response = failedResponse(request.RequestID, "invalid_request", fmt.Errorf("worker run is already complete")) - } else { - switch request.Operation { - case operationRun: - terminal = true - result, err := worker.run(ctx, *request.Run) - if err != nil { - response = failedResponse(request.RequestID, "invalid_or_failed_run", err) - } else { - terminal = true - response = workerResponse{SchemaVersion: workerResponseSchema, RequestID: request.RequestID, OK: true, Result: result} - } - case operationStreamStart: - started, result, err := worker.startFlatDirectStream(ctx, *request.Run) - if err != nil { - terminal = true - response = failedResponse(request.RequestID, "invalid_or_failed_stream_start", err) - } else { - stream = started - response = workerResponse{SchemaVersion: workerResponseSchema, RequestID: request.RequestID, OK: true, Result: result} - } - case operationStreamChunk: - if stream == nil { - response = failedResponse(request.RequestID, "invalid_request", fmt.Errorf("stream is not active")) - } else if result, err := stream.applyChunk(ctx, *request.Run); err != nil { - err = errors.Join(err, closeStream()) - terminal = true - response = failedResponse(request.RequestID, "invalid_or_failed_stream_chunk", err) - } else { - response = workerResponse{SchemaVersion: workerResponseSchema, RequestID: request.RequestID, OK: true, Result: result} - } - case operationStreamFinish: - if stream == nil { - response = failedResponse(request.RequestID, "invalid_request", fmt.Errorf("stream is not active")) - } else if status, err := stream.finish(ctx); err != nil { - err = errors.Join(err, closeStream()) - terminal = true - response = failedResponse(request.RequestID, "invalid_or_failed_stream_finish", err) - } else { - stream, terminal = nil, true - response = workerResponse{SchemaVersion: workerResponseSchema, RequestID: request.RequestID, OK: true, Stream: &status} - } - default: - panic("validated operation is not handled") - } - } - if err := encoder.Encode(response); err != nil { - return fmt.Errorf("encode worker response: %w", err) - } - } - if err := scanner.Err(); err != nil { - return fmt.Errorf("read bounded worker JSONL: %w", err) - } - if stream != nil { - return fmt.Errorf("RQ3 MALT worker ended before stream-finish") - } - if requests < 2 || !terminal { - return fmt.Errorf("RQ3 MALT worker ended before a complete run") - } - return nil -} -func decodeWorkerRequest(line []byte) (workerRequest, error) { - if len(bytes.TrimSpace(line)) == 0 { - return workerRequest{}, fmt.Errorf("JSONL record is empty") - } - if err := rejectDuplicateKeys(line); err != nil { - return workerRequest{}, err - } - decoder := json.NewDecoder(bytes.NewReader(line)) - decoder.DisallowUnknownFields() - var request workerRequest - if err := decoder.Decode(&request); err != nil { - return workerRequest{}, fmt.Errorf("decode strict worker request: %w", err) - } - var trailing any - if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { - if err == nil { - return workerRequest{}, fmt.Errorf("worker request contains trailing JSON") - } - return workerRequest{}, fmt.Errorf("decode worker request trailer: %w", err) - } - return request, nil -} - -func validateRequestEnvelope(request workerRequest, sequence int, capabilityPassed bool) error { - if request.SchemaVersion != workerRequestSchema { - return fmt.Errorf("schema_version must be %q", workerRequestSchema) - } - if request.RequestID == "" || len(request.RequestID) > 256 || strings.TrimSpace(request.RequestID) != request.RequestID || !utf8.ValidString(request.RequestID) { - return fmt.Errorf("request_id must be a bounded canonical string") - } - for _, character := range request.RequestID { - if character < 0x20 || character == 0x7f { - return fmt.Errorf("request_id contains a control character") - } - } - switch request.Operation { - case operationCapabilities: - if sequence != 1 || request.Run != nil { - return fmt.Errorf("capabilities must be the first request and omit run") - } - case operationRun, operationStreamStart: - if sequence != 2 || !capabilityPassed || request.Run == nil { - return fmt.Errorf("%s must follow capability preflight and include run", request.Operation) - } - case operationStreamChunk: - if sequence < 3 || !capabilityPassed || request.Run == nil { - return fmt.Errorf("stream-chunk must follow stream-start and include run") - } - case operationStreamFinish: - if sequence < 3 || !capabilityPassed || request.Run != nil { - return fmt.Errorf("stream-finish must follow stream-start and omit run") - } - default: - return fmt.Errorf("unsupported operation %q", request.Operation) - } - return nil -} -func failedResponse(requestID, code string, err error) workerResponse { - return workerResponse{ - SchemaVersion: workerResponseSchema, RequestID: requestID, OK: false, - Error: &workerError{Code: code, Message: err.Error()}, - } -} - -func rejectDuplicateKeys(data []byte) error { - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.UseNumber() - if err := scanJSONValue(decoder, "$"); err != nil { - return err - } - if token, err := decoder.Token(); !errors.Is(err, io.EOF) { - if err == nil { - return fmt.Errorf("trailing token %v", token) - } - return err - } - return nil -} - -func scanJSONValue(decoder *json.Decoder, location string) error { - token, err := decoder.Token() - if err != nil { - return err - } - delimiter, ok := token.(json.Delim) - if !ok { - return nil - } - switch delimiter { - case '{': - seen := make(map[string]struct{}) - for decoder.More() { - keyToken, err := decoder.Token() - if err != nil { - return err - } - key, ok := keyToken.(string) - if !ok { - return fmt.Errorf("%s has a non-string object key", location) - } - if _, duplicate := seen[key]; duplicate { - return fmt.Errorf("duplicate JSON key %q at %s", key, location) - } - seen[key] = struct{}{} - if err := scanJSONValue(decoder, location+"."+key); err != nil { - return err - } - } - _, err = decoder.Token() - return err - case '[': - index := 0 - for decoder.More() { - if err := scanJSONValue(decoder, fmt.Sprintf("%s[%d]", location, index)); err != nil { - return err - } - index++ - } - _, err = decoder.Token() - return err - default: - return fmt.Errorf("unexpected JSON delimiter %q", delimiter) - } -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/main_test.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/main_test.go deleted file mode 100644 index 2d1af3c..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/main_test.go +++ /dev/null @@ -1,746 +0,0 @@ -package main - -import ( - "context" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "math" - "os" - "slices" - "strings" - "testing" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq3baseline" - cid "github.com/ipfs/go-cid" -) - -func TestCleanupFailedPayloadFileReturnsCloseAndRemoveFailures(t *testing.T) { - primary := errors.New("write failed") - closeErr := errors.New("close failed") - removeErr := errors.New("remove failed") - got := cleanupFailedPayloadFile("payload", primary, closeErr, func(path string) error { - if path != "payload" { - t.Fatalf("remove path = %q", path) - } - return removeErr - }) - for _, want := range []error{primary, closeErr, removeErr} { - if !errors.Is(got, want) { - t.Fatalf("cleanup error %v does not include %v", got, want) - } - } - if got := cleanupFailedPayloadFile("payload", primary, nil, func(string) error { return os.ErrNotExist }); !errors.Is(got, primary) || errors.Is(got, os.ErrNotExist) { - t.Fatalf("not-exist cleanup should retain only the primary failure: %v", got) - } -} - -func TestClassifiedCASBatchesRespectBackendTransactionEnvelope(t *testing.T) { - blocks := []classifiedBlock{ - {block: clientcas.Block{Data: make([]byte, 4)}}, - {block: clientcas.Block{Data: make([]byte, 4)}}, - {block: clientcas.Block{Data: make([]byte, 1)}}, - } - end, err := nextClassifiedBlockBatchEnd(blocks, 0, 3, 8, 16) - if err != nil || end != 2 { - t.Fatalf("first batch end = %d, %v; want 2", end, err) - } - end, err = nextClassifiedBlockBatchEnd(blocks, end, 3, 8, 16) - if err != nil || end != 3 { - t.Fatalf("second batch end = %d, %v; want 3", end, err) - } - end, err = nextClassifiedBlockBatchEnd(blocks, 0, 1, 8, 16) - if err != nil || end != 1 { - t.Fatalf("block-count batch end = %d, %v; want 1", end, err) - } - if _, err := nextClassifiedBlockBatchEnd(blocks, 2, 3, 0, 16); err == nil { - t.Fatal("non-positive byte bound was accepted") - } - large := []classifiedBlock{ - {block: clientcas.Block{Data: make([]byte, 9)}}, - {block: clientcas.Block{Data: make([]byte, 1)}}, - } - if end, err := nextClassifiedBlockBatchEnd(large, 0, 2, 8, 16); err != nil || end != 1 { - t.Fatalf("large single-block batch end = %d, %v; want 1", end, err) - } - oversized := []classifiedBlock{{block: clientcas.Block{Data: make([]byte, 17)}}} - if _, err := nextClassifiedBlockBatchEnd(oversized, 0, 1, 8, 16); err == nil { - t.Fatal("oversized block was accepted") - } -} - -func TestLogicalPayloadStoreKeepsLiveStateDiskBacked(t *testing.T) { - store, err := newLogicalPayloadStore() - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if err := store.close(); err != nil { - t.Errorf("close logical payload store: %v", err) - } - }) - payload := []byte("disk-backed-payload") - digest := sha256.Sum256(payload) - file, err := store.retain(payload, 0o644, hex.EncodeToString(digest[:])) - if err != nil { - t.Fatal(err) - } - if file.data != nil { - t.Fatalf("logical payload store retained %d live heap bytes", len(file.data)) - } - stored, err := store.read(file) - if err != nil || !slices.Equal(stored, payload) { - t.Fatalf("disk-backed payload = %q, %v", stored, err) - } -} - -func TestLogicalPayloadStoreReconcileRetainsOnlyLiveCheckout(t *testing.T) { - store, err := newLogicalPayloadStore() - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if err := store.close(); err != nil { - t.Errorf("close logical payload store: %v", err) - } - }) - oldData, liveData := []byte("historical"), []byte("live") - oldSum, liveSum := sha256.Sum256(oldData), sha256.Sum256(liveData) - oldFile, err := store.retain(oldData, 0o644, hex.EncodeToString(oldSum[:])) - if err != nil { - t.Fatal(err) - } - liveFile, err := store.retain(liveData, 0o644, hex.EncodeToString(liveSum[:])) - if err != nil { - t.Fatal(err) - } - if err := store.reconcile(map[string]logicalFile{"file": liveFile}); err != nil { - t.Fatal(err) - } - if _, err := store.read(oldFile); !os.IsNotExist(err) { - t.Fatalf("historical payload read error = %v, want not-exist", err) - } - if got, err := store.read(liveFile); err != nil || string(got) != string(liveData) { - t.Fatalf("live payload = %q, %v", got, err) - } -} - -func TestLogicalPayloadOverlayDoesNotWarmMeasuredStore(t *testing.T) { - measured, err := newLogicalPayloadStore() - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if err := measured.close(); err != nil { - t.Errorf("close measured payload store: %v", err) - } - }) - base := []byte("base") - baseSum := sha256.Sum256(base) - baseFile, err := measured.retain(base, 0o644, hex.EncodeToString(baseSum[:])) - if err != nil { - t.Fatal(err) - } - overlay, err := newLogicalPayloadOverlay(measured) - if err != nil { - t.Fatal(err) - } - updated := []byte("updated") - updatedSum := sha256.Sum256(updated) - updatedFile, err := overlay.retain(updated, 0o644, hex.EncodeToString(updatedSum[:])) - if err != nil { - t.Fatal(err) - } - if got, err := overlay.read(baseFile); err != nil || string(got) != string(base) { - t.Fatalf("overlay fallback payload = %q, %v", got, err) - } - if err := overlay.close(); err != nil { - t.Fatal(err) - } - if _, err := measured.read(updatedFile); !os.IsNotExist(err) { - t.Fatalf("prevalidated payload leaked into measured store: %v", err) - } -} - -func TestDiskBackedFlatInsertCountsFullLogicalPayload(t *testing.T) { - store, err := newLogicalPayloadStore() - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if err := store.close(); err != nil { - t.Errorf("close logical payload store: %v", err) - } - }) - payload := []byte("new-file") - digest := sha256.Sum256(payload) - mode := uint32(0o644) - commit := rq3baseline.Commit{CommitID: "insert", Mutations: []rq3baseline.Mutation{{ - Kind: rq3baseline.MutationInsert, Path: "new.txt", FileKind: rq3baseline.FileKindRegular, - PayloadBase64: "bmV3LWZpbGU=", PayloadSHA256: hex.EncodeToString(digest[:]), Mode: &mode, - }}} - state := map[string]logicalFile{} - _, logicalBytes, _, err := applyFrozenCommit(state, commit, 4, true, store) - if err != nil { - t.Fatal(err) - } - if logicalBytes != int64(len(payload)) || state["new.txt"].data != nil { - t.Fatalf("disk-backed insert logical bytes=%d state=%#v", logicalBytes, state["new.txt"]) - } -} - -func TestParseFlagsRequiresDistinctBootstrapAuthorization(t *testing.T) { - instanceToken := strings.Repeat("1", 64) - bootstrapToken := strings.Repeat("2", 64) - config, err := parseFlags([]string{ - "-gateway-base-url", "http://127.0.0.1:9000", - "-gateway-instance-token", instanceToken, - "-gateway-bootstrap-authorization-token", bootstrapToken, - "-request-timeout", "1s", - }) - if err != nil { - t.Fatal(err) - } - if config.instanceToken != instanceToken || config.bootstrapAuthorizationToken != bootstrapToken { - t.Fatalf("config = %#v", config) - } - if _, err := parseFlags([]string{ - "-gateway-base-url", "http://127.0.0.1:9000", - "-gateway-instance-token", instanceToken, - "-gateway-bootstrap-authorization-token", instanceToken, - "-request-timeout", "1s", - }); err == nil { - t.Fatal("public instance token was accepted as bootstrap authorization") - } -} - -func TestControllerDirectoryIdentityRegistrationFailsClosed(t *testing.T) { - leased := directoryIdentity{Profile: directoryIdentityProfile, Device: 11, Inode: 22} - if err := validateRegisteredDirectoryIdentity(leased, leased); err != nil { - t.Fatal(err) - } - for name, registered := range map[string]directoryIdentity{ - "missing": {}, - "wrong profile": {Profile: "stale", Device: 11, Inode: 22}, - "wrong device": {Profile: directoryIdentityProfile, Device: 12, Inode: 22}, - "wrong inode": {Profile: directoryIdentityProfile, Device: 11, Inode: 23}, - "missing device": {Profile: directoryIdentityProfile, Inode: 22}, - } { - t.Run(name, func(t *testing.T) { - if err := validateRegisteredDirectoryIdentity(registered, leased); err == nil { - t.Fatal("missing or wrong registered directory identity was accepted") - } - }) - } -} - -func TestControllerResponseMustEchoExactDirectoryIdentity(t *testing.T) { - identity := directoryIdentity{Profile: directoryIdentityProfile, Device: 11, Inode: 22} - response := controllerResponse{ - SchemaVersion: controllerResponseSchema, RequestID: "start", OK: true, - CapabilityID: controllerCapabilityID, State: "ready", BindingSHA256: strings.Repeat("a", 64), - DirectoryIdentity: identity, - } - if err := validateSelfTestGatewayControllerResponse(response, "start", "ready", response.BindingSHA256, identity, false); err != nil { - t.Fatal(err) - } - wrong := response - wrong.DirectoryIdentity.Inode++ - if err := validateSelfTestGatewayControllerResponse(wrong, "start", "ready", response.BindingSHA256, identity, false); err == nil { - t.Fatal("controller response with the wrong directory identity was accepted") - } - stopped := response - stopped.RequestID, stopped.State, stopped.Quiesced = "stop", "stopped", true - if err := validateSelfTestGatewayControllerResponse(stopped, "stop", "stopped", response.BindingSHA256, identity, true); err != nil { - t.Fatal(err) - } -} - -func TestSubtreeRenameUpdatesAllBindingsIncrementallyAndMatchesFullOracle(t *testing.T) { - digest := func(value []byte) string { - sum := sha256.Sum256(value) - return hex.EncodeToString(sum[:]) - } - file := func(value string) logicalFile { - data := []byte(value) - return logicalFile{data: data, mode: 0o644, digest: digest(data)} - } - state := map[string]logicalFile{ - "old/a": file("aaaa"), - "old/nested/b": file("bbbbbbbb"), - "old/nested/c": file("cccc"), - "peer/file": file("peer"), - } - initial, err := directFlatSnapshotChanges(state) - if err != nil { - t.Fatal(err) - } - rootOracle, err := newFlatRootOracle(t.Context(), initial) - if err != nil { - t.Fatal(err) - } - mode := uint32(0o644) - commit := rq3baseline.Commit{CommitID: "subtree-rename", Mutations: []rq3baseline.Mutation{ - {Kind: rq3baseline.MutationRename, Path: "old/a", Destination: "new/a", FileKind: rq3baseline.FileKindRegular, ExpectedOldSHA256: digest([]byte("aaaa")), ExpectedOldMode: &mode, Mode: &mode}, - {Kind: rq3baseline.MutationRename, Path: "old/nested/b", Destination: "new/nested/b", FileKind: rq3baseline.FileKindRegular, ExpectedOldSHA256: digest([]byte("bbbbbbbb")), ExpectedOldMode: &mode, Mode: &mode}, - {Kind: rq3baseline.MutationRename, Path: "old/nested/c", Destination: "new/nested/c", FileKind: rq3baseline.FileKindRegular, ExpectedOldSHA256: digest([]byte("cccc")), ExpectedOldMode: &mode, Mode: &mode}, - }} - _, payloadBytes, changes, err := applyFrozenCommit(state, commit, 4, false, nil) - if err != nil { - t.Fatal(err) - } - if payloadBytes != 0 || len(changes) != 6 { - t.Fatalf("subtree rename payload=%d changes=%#v", payloadBytes, changes) - } - delta, err := directFlatDeltaChanges(changes) - if err != nil { - t.Fatal(err) - } - for _, change := range delta { - if string(change.Label) == "rq3/files/peer/file" || string(change.Label) == "rq3/modes/peer/file" { - t.Fatal("rename included unaffected peer binding") - } - } - next, err := rootOracle.apply(t.Context(), delta) - if err != nil { - t.Fatal(err) - } - if _, exists := state["old/a"]; exists || string(state["new/nested/b"].data) != "bbbbbbbb" { - t.Fatal("subtree rename source differs") - } - full, err := fullFlatRoot(t.Context(), state) - if err != nil || !next.Equals(full) { - t.Fatalf("incremental/full subtree rename: %s %s %v", next, full, err) - } - -} - -func TestGatewayDeleteAccountingPreservesAttemptAndReclamation(t *testing.T) { - accounting := exactAccounting{ - profile: gatewayAccountingProfile, available: true, method: gatewayByteMethod, - digest: strings.Repeat("a", 64), categories: []exactCategory{ - {category: gatewayCategories[0]}, - {category: gatewayCategories[1]}, - { - category: gatewayCategories[2], attemptedWrites: 1, attemptedBytes: 3, - attemptedDeleteWrites: 1, attemptedDeleteBytes: 3, deletedWrites: 1, - deletedReclaimedBytes: 8, reclaimedBytes: 8, netBytes: -8, - }, - }, - } - result := runResult{WriteEvents: []writeEvent{}} - if err := result.appendGatewayAccounting("commit", accounting); err != nil { - t.Fatal(err) - } - if len(result.WriteEvents) != 2 { - t.Fatalf("events = %#v", result.WriteEvents) - } - attempt, committed := result.WriteEvents[0], result.WriteEvents[1] - if attempt.Stage != stageAttempted || attempt.Disposition != dispositionDelete || attempt.Bytes != 3 || - committed.Stage != stageCommitted || committed.Disposition != dispositionDelete || committed.Bytes != 8 || - committed.GrossNewBytes != 0 || committed.ReclaimedBytes != 8 || committed.NetBytes != -8 || - attempt.ObjectKey != committed.ObjectKey { - t.Fatalf("delete lifecycle = %#v / %#v", attempt, committed) - } -} - -func TestGatewayAggregateRejectsUnpairableNewBytes(t *testing.T) { - accounting := exactAccounting{ - profile: gatewayAccountingProfile, available: true, method: gatewayByteMethod, - digest: strings.Repeat("b", 64), categories: []exactCategory{ - { - category: gatewayCategories[0], attemptedWrites: 1, attemptedBytes: 7, - attemptedNewWrites: 1, attemptedNewBytes: 7, newlyPersistedWrites: 1, - grossNewBytes: 8, newWrites: 1, newBytes: 8, netBytes: 8, - }, - {category: gatewayCategories[1]}, {category: gatewayCategories[2]}, - }, - } - result := runResult{WriteEvents: []writeEvent{}} - if err := result.appendGatewayAccounting("commit", accounting); err == nil || !strings.Contains(err.Error(), "do not bind") { - t.Fatalf("error = %v", err) - } -} - -func TestGatewayAggregatePreservesExactWriteCounts(t *testing.T) { - accounting := exactAccounting{ - profile: gatewayAccountingProfile, available: true, method: gatewayByteMethod, - digest: strings.Repeat("f", 64), categories: []exactCategory{ - { - category: gatewayCategories[0], attemptedWrites: 3, attemptedBytes: 21, - attemptedNewWrites: 3, attemptedNewBytes: 21, newlyPersistedWrites: 3, - grossNewBytes: 21, newWrites: 3, newBytes: 21, netBytes: 21, - }, - {category: gatewayCategories[1]}, {category: gatewayCategories[2]}, - }, - } - result := runResult{WriteEvents: []writeEvent{}} - if err := result.appendGatewayAccounting("commit", accounting); err != nil { - t.Fatal(err) - } - if len(result.WriteEvents) != 2 || result.WriteEvents[0].Count != 3 || result.WriteEvents[1].Count != 3 { - t.Fatalf("aggregate counts collapsed: %#v", result.WriteEvents) - } -} - -func TestCanonicalEmptySetupAccountingIsAttributedToFirstSnapshot(t *testing.T) { - setup := &runResult{PassMode: "accounting", WriteEvents: []writeEvent{ - { - Sequence: 99, CommitID: "setup-empty-top", Stage: stageAttempted, - Category: categoryCASMetadata, Cause: flatSentinelCause, - Disposition: dispositionNew, ObjectKey: "bafy/setup-flat-layout-sentinel", - Count: 1, Bytes: 12, CASClassification: casNew, - }, - { - Sequence: 100, CommitID: "setup-empty-top", Stage: stageCommitted, - Category: gatewayCategories[2], Cause: "gateway-authentication-object-ledger", - Disposition: dispositionNew, ObjectKey: "gateway-accounting/" + strings.Repeat("d", 64) + "/root-version-metadata/new", - Count: 1, Bytes: 20, GrossNewBytes: 20, NetBytes: 20, CASClassification: casNotApplicable, - }, - }} - result := &runResult{PassMode: "accounting", WriteEvents: []writeEvent{{ - CommitID: "snapshot", Stage: stageAttempted, Category: categoryLogicalPayload, - Cause: "snapshot-existing", Disposition: dispositionNew, ObjectKey: "existing", - Count: 1, Bytes: 1, CASClassification: casNew, - }}} - - attributeCanonicalEmptySetup(result, setup, "snapshot") - if len(result.WriteEvents) != 3 { - t.Fatalf("attributed events = %#v", result.WriteEvents) - } - for index, event := range result.WriteEvents { - if event.Sequence != uint64(index) { - t.Fatalf("event[%d] sequence = %d", index, event.Sequence) - } - if index == 0 { - continue - } - if event.CommitID != "snapshot" || !strings.HasPrefix(event.Cause, canonicalEmptySetupCause) { - t.Fatalf("setup event[%d] was not explicitly attributed: %#v", index, event) - } - } - if result.WriteEvents[1].ObjectKey != setup.WriteEvents[0].ObjectKey || result.WriteEvents[2].ObjectKey != setup.WriteEvents[1].ObjectKey { - t.Fatal("setup attribution changed exact lifecycle object keys") - } - if setup.WriteEvents[0].CommitID != "setup-empty-top" || setup.WriteEvents[0].Cause != flatSentinelCause { - t.Fatal("setup attribution mutated its input sink") - } - - timing := &runResult{PassMode: "timing", WriteEvents: []writeEvent{}} - attributeCanonicalEmptySetup(timing, &runResult{PassMode: "timing", WriteEvents: []writeEvent{}}, "snapshot") - if len(timing.WriteEvents) != 0 { - t.Fatal("timing pass gained setup accounting events") - } -} - -func TestGatewayAggregateRejectsCounterOverflow(t *testing.T) { - accounting := exactAccounting{ - profile: gatewayAccountingProfile, available: true, method: gatewayByteMethod, - digest: strings.Repeat("c", 64), categories: []exactCategory{ - { - category: gatewayCategories[0], attemptedWrites: math.MaxUint64, - attemptedNewWrites: math.MaxUint64, attemptedReplacementWrites: 1, - }, - {category: gatewayCategories[1]}, {category: gatewayCategories[2]}, - }, - } - result := runResult{WriteEvents: []writeEvent{}} - if err := result.appendGatewayAccounting("commit", accounting); err == nil || !strings.Contains(err.Error(), "overflows uint64") { - t.Fatalf("error = %v", err) - } -} - -func TestStrictWorkerRequestRejectsDuplicateKeysAndTrailingJSON(t *testing.T) { - for _, raw := range []string{ - `{"schema_version":"malt-rq3-malt-worker-request/v2","request_id":"one","request_id":"two","operation":"capabilities"}`, - `{"schema_version":"malt-rq3-malt-worker-request/v2","request_id":"one","operation":"capabilities"} {}`, - } { - if _, err := decodeWorkerRequest([]byte(raw)); err == nil { - t.Fatalf("hostile JSON accepted: %s", raw) - } - } -} - -func TestControlledListPayloadAccountingUsesChangedFixedChunks(t *testing.T) { - state := map[string]logicalFile{"payload.bin": {data: []byte("abcdefgh"), mode: 0o644}} - mode := uint32(0o644) - commit := rq3baseline.Commit{CommitID: "replace", Mutations: []rq3baseline.Mutation{{ - Kind: rq3baseline.MutationReplace, Path: "payload.bin", FileKind: rq3baseline.FileKindRegular, - PayloadBase64: "YWJjZFdYWVo=", PayloadSHA256: strings.Repeat("a", 64), - ExpectedOldSHA256: strings.Repeat("b", 64), ExpectedOldMode: &mode, Mode: &mode, - }}} - _, payloadBytes, _, err := applyFrozenCommit(state, commit, 4, true, nil) - if err != nil { - t.Fatal(err) - } - if payloadBytes != 4 { - t.Fatalf("logical payload bytes = %d, want one changed chunk", payloadBytes) - } -} - -func TestUnixFSConformanceRunsOnlyAfterAllMALTCommitMeasurements(t *testing.T) { - spec := rq3baseline.RunSpec{ - Snapshot: rq3baseline.Snapshot{CommitID: "snapshot"}, - Commits: []rq3baseline.Commit{{CommitID: "commit"}}, - } - result := &runResult{Commits: []commitRecord{ - {CommitID: "snapshot", HistoryRootsRetained: 1, NonWorkloadSetupRootsRetained: 1, LogicalObjectsChanged: 1, LogicalBindingsChanged: 1, AdapterPayloadInputBytes: 4}, - {CommitID: "commit", HistoryRootsRetained: 2, NonWorkloadSetupRootsRetained: 1, LogicalObjectsChanged: 1, LogicalBindingsChanged: 1, AdapterPayloadInputBytes: 8}, - }} - called := false - runner := func(context.Context, rq3baseline.RunSpec) (*rq3baseline.RunResult, error) { - called = true - if len(result.Commits) != len(spec.Commits)+1 { - t.Fatal("baseline runner observed an incomplete MALT result") - } - return &rq3baseline.RunResult{Records: []rq3baseline.CommitRecord{ - {CommitID: "snapshot", LogicalObjectsChanged: 1, LogicalBindingsChanged: 1, AdapterPayloadInputBytes: 4}, - {CommitID: "commit", LogicalObjectsChanged: 1, LogicalBindingsChanged: 1, AdapterPayloadInputBytes: 8}, - }}, nil - } - if err := postMeasurementBaselineConformance(t.Context(), spec, result, runner); err != nil { - t.Fatal(err) - } - if !called { - t.Fatal("post-measurement baseline conformance was not called") - } - - called = false - incomplete := &runResult{Commits: result.Commits[:1]} - if err := postMeasurementBaselineConformance(t.Context(), spec, incomplete, runner); err == nil || called { - t.Fatalf("incomplete result conformance: err=%v called=%v", err, called) - } -} - -func TestFlatGraphRepresentsEmptyRegularFileAsWholeBlobBinding(t *testing.T) { - initial, err := directFlatSnapshotChanges(map[string]logicalFile{"empty.txt": {data: []byte{}, mode: 0o644, digest: strings.Repeat("e", 64)}}) - if err != nil || len(initial) != 3 { - t.Fatalf("empty-file bindings: %+v %v", initial, err) - } - empty, err := clientcas.CIDForBlock(clientcas.Block{Codec: cid.Raw, Data: []byte{}}) - if err != nil { - t.Fatal(err) - } - found := false - for _, change := range initial { - if string(change.Label) == "rq3/files/empty.txt" { - found = change.After.Equals(empty) - } - } - if !found { - t.Fatal("empty file is not bound to the empty raw CID") - } - blocks, err := fileCASBlocks(logicalFile{data: []byte{}, mode: 0o644}, true, 4) - if err != nil { - t.Fatal(err) - } - if len(blocks) != 2 || blocks[0].cause != "flat-whole-file-blob" || len(blocks[0].block.Data) != 0 || blocks[1].cause != "file-mode-sidecar" { - t.Fatalf("empty file CAS blocks = %#v", blocks) - } -} - -func TestFlatMetadataCodecSeparatesEqualBytesFromRawPayloads(t *testing.T) { - mode, err := modeCASBlock(0o644) - if err != nil { - t.Fatal(err) - } - for _, metadata := range []classifiedBlock{flatSentinelBlock(), mode} { - if metadata.block.Codec != cid.DagJSON || !json.Valid(metadata.block.Data) { - t.Fatalf("metadata block %q is not valid DAG-JSON-domain JSON: %#v", metadata.cause, metadata.block) - } - metadataCID, err := clientcas.CIDForBlock(metadata.block) - if err != nil { - t.Fatal(err) - } - payloadCID, err := clientcas.CIDForBlock(clientcas.Block{Codec: cid.Raw, Data: append([]byte(nil), metadata.block.Data...)}) - if err != nil { - t.Fatal(err) - } - if metadataCID.Equals(payloadCID) { - t.Fatalf("metadata %q collides with an equal-byte raw payload: %s", metadata.cause, metadataCID) - } - } -} - -func TestFlatGraphNamespacesUserPathsAwayFromInternalMetadata(t *testing.T) { - files := map[string]logicalFile{ - "-eval/layout": {data: []byte("layout-name"), mode: 0o644}, - "other": {data: []byte("other"), mode: 0o644}, - "-eval/mode/b3RoZXI": {data: []byte("mode-name"), mode: 0o644}, - } - initial, err := directFlatSnapshotChanges(files) - if err != nil || len(initial) != 1+2*len(files) { - t.Fatalf("flat namespace: %+v %v", initial, err) - } - oracle, err := newFlatRootOracle(t.Context(), initial) - if err != nil { - t.Fatal(err) - } - before := files["-eval/layout"] - after := logicalFile{data: []byte("updated"), mode: 0o644} - changes, err := directFlatDeltaChanges([]fileChange{{path: "-eval/layout", before: &before, after: &after}}) - if err != nil { - t.Fatal(err) - } - for _, change := range changes { - if string(change.Label) == "@malt-eval/layout" { - t.Fatal("user path changed internal sentinel") - } - } - next, err := oracle.apply(t.Context(), changes) - if err != nil { - t.Fatal(err) - } - files["-eval/layout"] = after - full, err := fullFlatRoot(t.Context(), files) - if err != nil || !full.Equals(next) { - t.Fatal("flat namespace update differs from complete rebuild") - } - -} - -func TestFlatRootOracleMatchesFullRootAndRejectsDifferentGatewayRoot(t *testing.T) { - digest := func(value []byte) string { - sum := sha256.Sum256(value) - return hex.EncodeToString(sum[:]) - } - state := map[string]logicalFile{ - "file": {data: []byte("base"), mode: 0o644}, - } - initial, err := directFlatSnapshotChanges(state) - if err != nil { - t.Fatal(err) - } - oracle, err := newFlatRootOracle(t.Context(), initial) - if err != nil { - t.Fatal(err) - } - mode := uint32(0o644) - commit := rq3baseline.Commit{CommitID: "replace", Mutations: []rq3baseline.Mutation{{ - Kind: rq3baseline.MutationReplace, Path: "file", FileKind: rq3baseline.FileKindRegular, - PayloadBase64: base64.StdEncoding.EncodeToString([]byte("next")), PayloadSHA256: digest([]byte("next")), - ExpectedOldSHA256: digest([]byte("base")), ExpectedOldMode: &mode, Mode: &mode, - }}} - _, _, logicalChanges, err := applyFrozenCommit(state, commit, 4, true, nil) - if err != nil { - t.Fatal(err) - } - changes, err := directFlatDeltaChanges(logicalChanges) - if err != nil { - t.Fatal(err) - } - expected, err := oracle.apply(t.Context(), changes) - if err != nil { - t.Fatal(err) - } - full, err := fullFlatRoot(t.Context(), state) - if err != nil || !expected.Equals(full) { - t.Fatalf("incremental=%s full=%s err=%v", expected, full, err) - } - if err := verifyFlatGatewayRoot("test", expected, expected); err != nil { - t.Fatal(err) - } - if err := verifyFlatGatewayRoot("test", expected, cid.Undef); err == nil { - t.Fatal("different Gateway root was accepted") - } -} - -func TestFlatRootOracleExportsOnlyCurrentMaterialization(t *testing.T) { - blockCID := func(value string) cid.Cid { - key, err := clientcas.CIDForBlock(clientcas.Block{Codec: cid.Raw, Data: []byte(value)}) - if err != nil { - t.Fatal(err) - } - return key - } - path := []byte("rq3/files/file") - current := blockCID("initial") - oracle, err := newFlatRootOracle(t.Context(), []gatewaytransport.FlatPrefixChange{{Label: path, After: current}}) - if err != nil { - t.Fatal(err) - } - for index := 0; index < 128; index++ { - next := blockCID(fmt.Sprintf("version-%d", index)) - if _, err := oracle.apply(t.Context(), []gatewaytransport.FlatPrefixChange{{Label: path, Before: current, After: next}}); err != nil { - t.Fatal(err) - } - current = next - } - candidate, err := oracle.writer.Export(t.Context()) - if err != nil { - t.Fatal(err) - } - if len(candidate.State.Entries) != 1 || len(candidate.Nodes) != 1 || !candidate.State.Entries[0].Target.Equals(current) { - t.Fatal("flat oracle retained obsolete entries or exported historical roots") - } - -} - -func TestGitReplacementToZeroBytesPersistsEmptyFlatBlob(t *testing.T) { - mode := uint32(0o644) - state := map[string]logicalFile{ - "file.txt": {data: []byte("x"), mode: mode, digest: strings.Repeat("1", 64)}, - } - commit := rq3baseline.Commit{CommitID: "empty", Mutations: []rq3baseline.Mutation{{ - Kind: rq3baseline.MutationReplace, Path: "file.txt", FileKind: rq3baseline.FileKindRegular, - PayloadBase64: "", PayloadSHA256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - ExpectedOldSHA256: strings.Repeat("1", 64), ExpectedOldMode: &mode, Mode: &mode, - }}} - blocks, payloadBytes, _, err := applyFrozenCommit(state, commit, 4, true, nil) - if err != nil { - t.Fatal(err) - } - if payloadBytes != 0 || len(blocks) != 1 || blocks[0].cause != "flat-whole-file-blob" || len(blocks[0].block.Data) != 0 || len(state["file.txt"].data) != 0 { - t.Fatalf("zero-byte Git replace: payload=%d blocks=%#v state=%#v", payloadBytes, blocks, state["file.txt"]) - } -} - -func TestWorkloadIdentityRejectsNoncanonicalControlledCoordinates(t *testing.T) { - identity := workloadIdentity{ - ID: "controlled", Kind: "controlled", ArtifactSHA256: strings.Repeat("1", 64), - SemanticSHA256: strings.Repeat("2", 64), ChunkBytes: 4, CommitListSHA256: strings.Repeat("3", 64), - HistoryRetention: "all-roots", ControlledStructure: "map", - ControlledCoordinate: &controlledCoordinate{Operation: "replace", PathDepth: 1, DirectoryWidth: 1, FileChunks: 1, BatchSize: 1}, - } - if err := validateWorkloadIdentity(identity); err != nil { - t.Fatal(err) - } - identity.ControlledCoordinate.Operation = "Replace" - if err := validateWorkloadIdentity(identity); err == nil { - t.Fatal("uppercase coordinate operation was accepted") - } - identity.ControlledCoordinate.Operation = "replace" - identity.ControlledCoordinate.PathDepth = 0 - if err := validateWorkloadIdentity(identity); err == nil { - t.Fatal("zero coordinate axis was accepted") - } -} - -func TestCommitManifestBindsCompleteOrderBeforeStreaming(t *testing.T) { - commits := []string{"snapshot", "commit-a", "commit-b"} - encoded, err := json.Marshal(struct { - Commits []string `json:"commits"` - }{Commits: commits}) - if err != nil { - t.Fatal(err) - } - digest := sha256.Sum256(encoded) - identity := workloadIdentity{CommitListSHA256: hex.EncodeToString(digest[:])} - if err := validateCommitManifest(identity, commits); err != nil { - t.Fatal(err) - } - wrong := append([]string(nil), commits...) - wrong[1], wrong[2] = wrong[2], wrong[1] - if err := validateCommitManifest(identity, wrong); err == nil { - t.Fatal("reordered stream commit manifest was accepted") - } - duplicate := []string{"snapshot", "commit-a", "commit-a"} - if err := validateCommitManifest(identity, duplicate); err == nil { - t.Fatal("duplicate stream commit manifest was accepted") - } -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/model.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/model.go deleted file mode 100644 index 1375010..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/model.go +++ /dev/null @@ -1,243 +0,0 @@ -package main - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "slices" - - "github.com/dewebprotocol/malt-client/internal/evaluation/rq3baseline" - "github.com/dewebprotocol/malt-client/transport" - cid "github.com/ipfs/go-cid" -) - -const ( - categoryLogicalPayload = "logical-changed-payload" - categoryCASMetadata = "cas-structural-metadata" - modeSidecarSchema = "malt-eval-rq3-file-mode/v1" -) - -type logicalFile struct { - data []byte - mode uint32 - digest string - size int64 - payload cid.Cid -} - -type classifiedBlock struct { - block transport.Block - category string - cause string - suffix string -} - -type fileChange struct { - path string - before *logicalFile - after *logicalFile -} - -func initialLogicalState(snapshot rq3baseline.Snapshot, chunkBytes uint64, payloads *logicalPayloadStore) (map[string]logicalFile, []classifiedBlock, error) { - state := make(map[string]logicalFile, len(snapshot.Files)) - blocks := make([]classifiedBlock, 0) - for _, file := range snapshot.Files { - data, err := decodeFrozenPayload(file.PayloadBase64) - if err != nil || file.Mode == nil { - return nil, nil, fmt.Errorf("decode prevalidated snapshot file %q: %w", file.Path, err) - } - value, err := payloads.retain(data, *file.Mode, file.PayloadSHA256) - if err != nil { - return nil, nil, fmt.Errorf("retain snapshot file %q: %w", file.Path, err) - } - state[file.Path] = value - materialized := value - materialized.data = data - fileBlocks, err := fileCASBlocks(materialized, true, chunkBytes) - if err != nil { - return nil, nil, fmt.Errorf("normalize snapshot file %q: %w", file.Path, err) - } - blocks = append(blocks, fileBlocks...) - } - return state, blocks, nil -} - -// applyFrozenCommit executes the already baseline-validated source semantics -// in their frozen order. CAS attempts retain intermediate full-result payloads -// even though one client root closes the complete commit. -func applyFrozenCommit(state map[string]logicalFile, commit rq3baseline.Commit, chunkBytes uint64, changedChunksOnly bool, payloads *logicalPayloadStore) ([]classifiedBlock, int64, []fileChange, error) { - blocks := make([]classifiedBlock, 0) - var logicalPayloadBytes int64 - before := make(map[string]*logicalFile) - capture := func(path string) { - if _, captured := before[path]; captured { - return - } - if file, exists := state[path]; exists { - copy := file - before[path] = © - } else { - before[path] = nil - } - } - for index, change := range commit.Mutations { - capture(change.Path) - if change.Destination != "" { - capture(change.Destination) - } - old := state[change.Path] - switch change.Kind { - case rq3baseline.MutationInsert, rq3baseline.MutationReplace, rq3baseline.MutationAppend: - data, err := decodeFrozenPayload(change.PayloadBase64) - if err != nil || change.Mode == nil { - return nil, 0, nil, fmt.Errorf("mutation[%d] decode payload: %w", index, err) - } - next, err := payloads.retain(data, *change.Mode, change.PayloadSHA256) - if err != nil { - return nil, 0, nil, fmt.Errorf("mutation[%d] retain payload: %w", index, err) - } - includeMode := change.Kind == rq3baseline.MutationInsert || old.mode != next.mode - materialized := next - materialized.data = data - fileBlocks, err := fileCASBlocks(materialized, includeMode, chunkBytes) - if err != nil { - return nil, 0, nil, fmt.Errorf("mutation[%d] normalize file: %w", index, err) - } - blocks = append(blocks, fileBlocks...) - var changed int64 - switch change.Kind { - case rq3baseline.MutationInsert: - changed = int64(len(data)) - case rq3baseline.MutationAppend: - changed = int64(len(data)) - old.size - case rq3baseline.MutationReplace: - if changedChunksOnly { - oldData, readErr := payloads.read(old) - if readErr != nil { - return nil, 0, nil, fmt.Errorf("mutation[%d] read old payload: %w", index, readErr) - } - changed, err = changedFixedChunkBytes(oldData, data, chunkBytes) - } else { - changed = int64(len(data)) - } - } - if err != nil { - return nil, 0, nil, fmt.Errorf("mutation[%d] logical payload accounting: %w", index, err) - } - if changed < 0 || logicalPayloadBytes > int64(^uint64(0)>>1)-changed { - return nil, 0, nil, fmt.Errorf("mutation[%d] logical payload accounting overflow", index) - } - logicalPayloadBytes += changed - state[change.Path] = next - - case rq3baseline.MutationModeChange: - if change.Mode == nil { - return nil, 0, nil, fmt.Errorf("mutation[%d] mode is absent after semantic prevalidation", index) - } - old.mode = *change.Mode - state[change.Path] = old - modeBlock, err := modeCASBlock(old.mode) - if err != nil { - return nil, 0, nil, err - } - blocks = append(blocks, modeBlock) - - case rq3baseline.MutationDelete: - delete(state, change.Path) - - case rq3baseline.MutationRename, rq3baseline.MutationMove: - delete(state, change.Path) - state[change.Destination] = old - - default: - return nil, 0, nil, fmt.Errorf("mutation[%d] kind %q escaped semantic prevalidation", index, change.Kind) - } - } - paths := make([]string, 0, len(before)) - for path := range before { - paths = append(paths, path) - } - slices.Sort(paths) - changes := make([]fileChange, 0, len(paths)) - for _, path := range paths { - var after *logicalFile - if file, exists := state[path]; exists { - copy := file - after = © - } - if logicalFileEqual(before[path], after) { - continue - } - changes = append(changes, fileChange{path: path, before: before[path], after: after}) - } - return blocks, logicalPayloadBytes, changes, nil -} - -func logicalFileEqual(a, b *logicalFile) bool { - if a == nil || b == nil { - return a == nil && b == nil - } - return a.mode == b.mode && a.digest == b.digest && a.size == b.size && a.payload.Equals(b.payload) -} - -func changedFixedChunkBytes(before, after []byte, chunkBytes uint64) (int64, error) { - if chunkBytes == 0 || chunkBytes > uint64(^uint(0)>>1) { - return 0, fmt.Errorf("chunk size is outside host bounds") - } - size := int(chunkBytes) - var total int64 - for offset := 0; offset < len(after); offset += size { - end := min(len(after), offset+size) - unchanged := end <= len(before) && slices.Equal(before[offset:end], after[offset:end]) - if unchanged { - continue - } - if total > int64(^uint64(0)>>1)-int64(end-offset) { - return 0, fmt.Errorf("changed chunk bytes overflow") - } - total += int64(end - offset) - } - return total, nil -} - -func decodeFrozenPayload(encoded string) ([]byte, error) { - value, err := base64.StdEncoding.Strict().DecodeString(encoded) - if err != nil || base64.StdEncoding.EncodeToString(value) != encoded { - return nil, fmt.Errorf("payload is not canonical standard base64") - } - return value, nil -} - -func fileCASBlocks(file logicalFile, includeMode bool, chunkBytes uint64) ([]classifiedBlock, error) { - if chunkBytes == 0 { - return nil, fmt.Errorf("fixed chunk size is zero") - } - result := []classifiedBlock{{ - block: transport.Block{Codec: cid.Raw, Data: append([]byte(nil), file.data...)}, - category: categoryLogicalPayload, cause: "flat-whole-file-blob", suffix: "payload", - }} - if includeMode { - mode, err := modeCASBlock(file.mode) - if err != nil { - return nil, err - } - result = append(result, mode) - } - return result, nil -} - -func modeCASBlock(mode uint32) (classifiedBlock, error) { - wire := struct { - SchemaVersion string `json:"schema_version"` - FileKind string `json:"file_kind"` - Mode uint32 `json:"mode"` - }{SchemaVersion: modeSidecarSchema, FileKind: rq3baseline.FileKindRegular, Mode: mode} - raw, err := json.Marshal(wire) - if err != nil { - return classifiedBlock{}, err - } - return classifiedBlock{ - block: transport.Block{Codec: cid.DagJSON, Data: raw}, category: categoryCASMetadata, - cause: "file-mode-sidecar", suffix: "mode-sidecar", - }, nil -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/payload_store.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/payload_store.go deleted file mode 100644 index d76716d..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/payload_store.go +++ /dev/null @@ -1,179 +0,0 @@ -package main - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "os" - "path/filepath" - - clientcas "github.com/dewebprotocol/malt-client/internal/cas" - "github.com/dewebprotocol/malt-client/transport" - cid "github.com/ipfs/go-cid" -) - -type logicalPayloadStore struct { - directory string - parent *logicalPayloadStore - refs map[string]uint64 -} - -func newLogicalPayloadStore() (*logicalPayloadStore, error) { - directory, err := os.MkdirTemp("", "malt-rq3-flat-payloads-") - if err != nil { - return nil, err - } - return &logicalPayloadStore{directory: directory, refs: make(map[string]uint64)}, nil -} - -// newLogicalPayloadOverlay gives chunk prevalidation a private, bounded spill -// directory while allowing it to read the already-live measured checkout. This -// prevents prevalidation from warming or permanently growing the measured -// payload store. -func newLogicalPayloadOverlay(parent *logicalPayloadStore) (*logicalPayloadStore, error) { - store, err := newLogicalPayloadStore() - if err != nil { - return nil, err - } - store.parent = parent - return store, nil -} - -func (s *logicalPayloadStore) retain(data []byte, mode uint32, digest string) (logicalFile, error) { - payload, err := clientcas.CIDForBlock(transport.Block{Codec: cid.Raw, Data: data}) - if err != nil { - return logicalFile{}, err - } - if s == nil { - return logicalFile{data: append([]byte(nil), data...), mode: mode, digest: digest, size: int64(len(data)), payload: payload}, nil - } - if s.directory == "" { - return logicalFile{}, fmt.Errorf("MALT-flat logical payload store is closed") - } - actual := sha256.Sum256(data) - if hex.EncodeToString(actual[:]) != digest { - return logicalFile{}, fmt.Errorf("MALT-flat logical payload %q has a mismatched digest", digest) - } - if s.refs[digest] > 0 { - s.refs[digest]++ - return logicalFile{mode: mode, digest: digest, size: int64(len(data)), payload: payload}, nil - } - path := filepath.Join(s.directory, digest) - file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) - if os.IsExist(err) { - existing, readErr := os.ReadFile(path) - if readErr != nil { - return logicalFile{}, readErr - } - if !bytes.Equal(existing, data) { - return logicalFile{}, fmt.Errorf("MALT-flat logical payload %q differs from its existing content", digest) - } - s.refs[digest]++ - return logicalFile{mode: mode, digest: digest, size: int64(len(data)), payload: payload}, nil - } - if err != nil { - return logicalFile{}, err - } - if _, err := file.Write(data); err != nil { - return logicalFile{}, cleanupFailedPayloadFile(path, err, file.Close(), os.Remove) - } - if err := file.Close(); err != nil { - return logicalFile{}, cleanupFailedPayloadFile(path, err, nil, os.Remove) - } - s.refs[digest]++ - return logicalFile{mode: mode, digest: digest, size: int64(len(data)), payload: payload}, nil -} - -func cleanupFailedPayloadFile(path string, primary, closeErr error, remove func(string) error) error { - removeErr := remove(path) - if os.IsNotExist(removeErr) { - removeErr = nil - } - return errors.Join(primary, closeErr, removeErr) -} - -func (s *logicalPayloadStore) read(file logicalFile) ([]byte, error) { - if file.data != nil { - return file.data, nil - } - if s == nil || s.directory == "" { - return nil, fmt.Errorf("MALT-flat logical payload store is closed") - } - data, err := os.ReadFile(filepath.Join(s.directory, file.digest)) - if os.IsNotExist(err) && s.parent != nil { - return s.parent.read(file) - } - if err != nil { - return nil, err - } - digest := sha256.Sum256(data) - if int64(len(data)) != file.size || hex.EncodeToString(digest[:]) != file.digest { - return nil, fmt.Errorf("MALT-flat logical payload %q failed validation", file.digest) - } - return data, nil -} - -// reconcile retains exactly the immutable payloads referenced by the supplied -// live checkout states. Intermediate and replaced payloads are removed between -// commits, bounding disk use by the current checkout rather than full history. -func (s *logicalPayloadStore) reconcile(states ...map[string]logicalFile) error { - if s == nil || s.directory == "" { - return fmt.Errorf("MALT-flat logical payload store is closed") - } - live := make(map[string]uint64) - for _, state := range states { - for _, file := range state { - if file.digest == "" { - return fmt.Errorf("MALT-flat live payload has an empty digest") - } - live[file.digest]++ - } - } - var reconcileErr error - for digest := range s.refs { - if live[digest] != 0 { - continue - } - if err := os.Remove(filepath.Join(s.directory, digest)); err != nil && !os.IsNotExist(err) { - reconcileErr = errors.Join(reconcileErr, fmt.Errorf("remove unreferenced MALT-flat logical payload %q: %w", digest, err)) - continue - } - delete(s.refs, digest) - } - for digest, count := range live { - if _, err := os.Stat(filepath.Join(s.directory, digest)); err != nil { - reconcileErr = errors.Join(reconcileErr, fmt.Errorf("stat live MALT-flat logical payload %q: %w", digest, err)) - continue - } - s.refs[digest] = count - } - return reconcileErr -} - -func (s *logicalPayloadStore) close() error { - if s == nil || s.directory == "" { - return nil - } - directory := s.directory - if err := os.RemoveAll(directory); err != nil { - return err - } - s.directory = "" - s.refs = nil - s.parent = nil - return nil -} - -func retryMALTCleanup(close func() error) error { - if close == nil { - return nil - } - if err := close(); err != nil { - if retryErr := close(); retryErr != nil { - return errors.Join(err, retryErr) - } - } - return nil -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/role_index.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/role_index.go deleted file mode 100644 index ce9f133..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/role_index.go +++ /dev/null @@ -1,116 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "os" - - "github.com/syndtr/goleveldb/leveldb" - "github.com/syndtr/goleveldb/leveldb/opt" -) - -const casRoleIndexProfile = "process-temporary-leveldb-bounded-memory-unmeasured/v1" - -type blockRole struct { - key string - category string -} - -// blockRoleIndex keeps the cross-category CAS invariant exact with bounded -// block-cache and memtable memory instead of retaining every historical CID in -// the worker heap. Its process-temporary disk grows O(unique CIDs) and is -// removed on close. The Gateway's observed PutBatch disposition remains the -// source of write accounting, and this index is never included in the measured -// FSKV. -type blockRoleIndex struct { - directory string - database *leveldb.DB -} - -func newBlockRoleIndex() (*blockRoleIndex, error) { - directory, err := os.MkdirTemp("", "malt-rq3-cas-role-index-") - if err != nil { - return nil, err - } - database, err := leveldb.OpenFile(directory, &opt.Options{ - BlockCacheCapacity: 4 << 20, - BlockCacheEvictRemoved: true, - CompactionTableSize: 4 << 20, - OpenFilesCacheCapacity: 16, - WriteBuffer: 4 << 20, - CompactionTotalSize: 32 << 20, - NoSync: true, - }) - if err != nil { - return nil, errors.Join(err, os.RemoveAll(directory)) - } - return &blockRoleIndex{directory: directory, database: database}, nil -} - -func (index *blockRoleIndex) checkAndRecord(key, category string) error { - return index.checkAndRecordBatch([]blockRole{{key: key, category: category}}) -} - -func (index *blockRoleIndex) checkAndRecordBatch(roles []blockRole) error { - if index == nil || index.database == nil || index.directory == "" { - return errors.New("CAS role index is not open") - } - if len(roles) == 0 { - return nil - } - pending := make(map[string]string, len(roles)) - batch := new(leveldb.Batch) - writes := 0 - for _, role := range roles { - if role.key == "" || role.category == "" { - return errors.New("CAS role index received an empty key/category") - } - if previous, ok := pending[role.key]; ok { - if previous != role.category { - return fmt.Errorf("CAS object %s crosses mutually exclusive categories %q and %q", role.key, previous, role.category) - } - continue - } - previous, err := index.database.Get([]byte(role.key), nil) - switch { - case err == nil: - if string(previous) != role.category { - return fmt.Errorf("CAS object %s crosses mutually exclusive categories %q and %q", role.key, string(previous), role.category) - } - pending[role.key] = role.category - case errors.Is(err, leveldb.ErrNotFound): - pending[role.key] = role.category - batch.Put([]byte(role.key), []byte(role.category)) - writes++ - default: - return fmt.Errorf("read CAS role for %s: %w", role.key, err) - } - } - if writes == 0 { - return nil - } - if err := index.database.Write(batch, nil); err != nil { - return fmt.Errorf("record CAS role batch: %w", err) - } - return nil -} - -func (index *blockRoleIndex) close() error { - if index == nil { - return nil - } - directory := index.directory - var closeErr error - if index.database != nil { - closeErr = index.database.Close() - index.database = nil - } - var removeErr error - if directory != "" { - removeErr = os.RemoveAll(directory) - if removeErr == nil { - index.directory = "" - } - } - return errors.Join(closeErr, removeErr) -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/role_index_test.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/role_index_test.go deleted file mode 100644 index fb74713..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/role_index_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package main - -import ( - "crypto/sha256" - "encoding/binary" - "encoding/hex" - "os" - "path/filepath" - "runtime" - "strings" - "testing" -) - -func TestBlockRoleIndexPersistsExactCategoriesAndCleansUp(t *testing.T) { - t.Setenv("TMPDIR", t.TempDir()) - index, err := newBlockRoleIndex() - if err != nil { - t.Fatal(err) - } - directory := index.directory - if err := index.checkAndRecord("bafy-role", "logical-changed-payload"); err != nil { - t.Fatal(err) - } - if err := index.checkAndRecord("bafy-role", "logical-changed-payload"); err != nil { - t.Fatalf("same category must be idempotent: %v", err) - } - if err := index.checkAndRecord("bafy-role", "cas-structural-metadata"); err == nil || !strings.Contains(err.Error(), "crosses mutually exclusive categories") { - t.Fatalf("cross-category reuse error = %v", err) - } - if err := index.close(); err != nil { - t.Fatal(err) - } - if _, err := os.Lstat(directory); !os.IsNotExist(err) { - t.Fatalf("temporary role index remains after close: %v", err) - } - if err := index.close(); err != nil { - t.Fatalf("close must be idempotent: %v", err) - } - if err := index.checkAndRecord("bafy-after-close", "logical-changed-payload"); err == nil { - t.Fatal("closed role index accepted a write") - } -} - -func TestBlockRoleIndexLargeHistoryUsesBoundedHeapAndTemporaryDisk(t *testing.T) { - t.Setenv("TMPDIR", t.TempDir()) - index, err := newBlockRoleIndex() - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { _ = index.close() }) - directory := index.directory - - runtime.GC() - var before runtime.MemStats - runtime.ReadMemStats(&before) - const entries = 400_000 - const batchSize = 1_024 - for start := 0; start < entries; start += batchSize { - end := min(start+batchSize, entries) - roles := make([]blockRole, end-start) - for offset := range roles { - var input [8]byte - binary.LittleEndian.PutUint64(input[:], uint64(start+offset)) - digest := sha256.Sum256(input[:]) - roles[offset] = blockRole{key: hex.EncodeToString(digest[:]), category: "logical-changed-payload"} - } - if err := index.checkAndRecordBatch(roles); err != nil { - t.Fatal(err) - } - } - runtime.GC() - var after runtime.MemStats - runtime.ReadMemStats(&after) - var heapGrowth uint64 - if after.HeapAlloc > before.HeapAlloc { - heapGrowth = after.HeapAlloc - before.HeapAlloc - } - if heapGrowth > 32<<20 { - t.Fatalf("role index retained %d heap bytes for %d unique CIDs", heapGrowth, entries) - } - - var diskBytes int64 - if err := filepath.Walk(directory, func(_ string, info os.FileInfo, walkErr error) error { - if walkErr != nil { - return walkErr - } - if info.Mode().IsRegular() { - diskBytes += info.Size() - } - return nil - }); err != nil { - t.Fatal(err) - } - if diskBytes == 0 { - t.Fatal("large role history did not use its declared process-temporary disk") - } - t.Logf("unique_cids=%d heap_growth_bytes=%d temporary_disk_bytes=%d", entries, heapGrowth, diskBytes) - - if err := index.close(); err != nil { - t.Fatal(err) - } - if _, err := os.Lstat(directory); !os.IsNotExist(err) { - t.Fatalf("large temporary role index remains after close: %v", err) - } -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/selftest.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/selftest.go deleted file mode 100644 index 726988a..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/selftest.go +++ /dev/null @@ -1,1037 +0,0 @@ -package main - -import ( - "bufio" - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "net" - "os" - "os/exec" - "path/filepath" - "reflect" - "slices" - "strings" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/e0selftest" - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq3baseline" - cid "github.com/ipfs/go-cid" -) - -const ( - maltAdapterSelfTestCorpusSchema = "malt-eval-rq3-malt-adapter-self-test/v8" - controllerRequestSchema = "malt-rq3-gateway-controller-request/v3" - controllerResponseSchema = "malt-rq3-gateway-controller-response/v2" - controllerCapabilityID = "rq3.gateway-lifecycle-controller.v2" - directoryIdentityProfile = "physical-directory-posix-device-inode/v1" -) - -var maltAdapterSelfTestProfile = e0selftest.Profile{ - ProfileID: "rq3-malt-adapter-positive-hostile-v8", - PositiveCases: []string{ - "execute-controlled-malt-flat", - "execute-git-first-parent-malt-flat", - }, - HostileCases: []string{ - "reject-malformed-request", - "reject-tampered-payload", - "reject-stale-before-value", - "reject-commit-list-digest-mismatch", - "reject-commit-manifest-before-gateway-side-effects", - "reject-valid-wrong-gateway-root-immediately", - "reject-unfinished-stream", - "reject-failed-stream-reuse", - "reject-active-stream-binding-reuse", - }, -} - -type maltSelfTestConfig struct { - corpusPath string - controllerPath string - cleanPath string - optionsPath string - requestTimeout time.Duration - boundWorkloads []string - gitExecutables []string -} - -type repeatedPathFlag []string - -func (f *repeatedPathFlag) String() string { return strings.Join(*f, ",") } -func (f *repeatedPathFlag) Set(value string) error { - *f = append(*f, value) - return nil -} - -type maltAdapterSelfTestCorpus struct { - SchemaVersion string `json:"schema_version"` - PositiveCases []maltAdapterSelfTestPositiveCase `json:"positive_cases"` - HostileCases []maltAdapterSelfTestHostileCase `json:"hostile_cases"` -} - -type maltAdapterSelfTestPositiveCase struct { - ID string `json:"id"` - SourceKind string `json:"source_kind"` - Run runSpec `json:"run"` -} - -type observingEvaluationGateway struct { - base evaluationGateway - healthCalls int - flatPrefixCalls int - corruptFlatPrefixCall int - firstFlatRoot cid.Cid -} - -func (g *observingEvaluationGateway) Health(ctx context.Context) (*gatewaytransport.Health, error) { - g.healthCalls++ - return g.base.Health(ctx) -} - -func (g *observingEvaluationGateway) ApplyEvaluationFlatPrefix(ctx context.Context, authorization string, mutation gatewaytransport.FlatPrefixMutation) (gatewaytransport.FlatPrefixResult, error) { - g.flatPrefixCalls++ - result, err := g.base.ApplyEvaluationFlatPrefix(ctx, authorization, mutation) - if err != nil { - return result, err - } - if g.flatPrefixCalls == 1 { - g.firstFlatRoot = result.Root - } - if g.corruptFlatPrefixCall == g.flatPrefixCalls && g.firstFlatRoot.Defined() { - result.Root = g.firstFlatRoot - } - return result, nil -} - -type maltAdapterSelfTestHostileCase struct { - ID string `json:"id"` - Kind string `json:"kind"` -} - -type controllerArtifactPin struct { - Path string `json:"path"` - SHA256 string `json:"sha256"` - Bytes int64 `json:"bytes"` -} - -type controllerEngineConfiguration struct { - Engine string `json:"engine"` - Version string `json:"version"` - OptionsArtifactSHA256 string `json:"options_artifact_sha256"` - SyncWrites bool `json:"sync_writes"` - WALPolicy string `json:"wal_policy"` - LSMPolicy string `json:"lsm_policy"` - CompactionPolicy string `json:"compaction_policy"` - GCPolicy string `json:"gc_policy"` - PrimaryStorageScope string `json:"primary_storage_scope"` - LookupIndexProfile string `json:"lookup_index_profile"` -} - -type controllerStartBinding struct { - System string `json:"system"` - WorkloadID string `json:"workload_id"` - PassMode string `json:"pass_mode"` - RunPhase string `json:"run_phase"` - ClusterID string `json:"cluster_id"` - RunIndex int `json:"run_index"` - Directory string `json:"directory"` - DirectoryID string `json:"directory_id"` - DirectoryIdentity directoryIdentity `json:"directory_identity"` - BaseURL string `json:"base_url"` - InstanceToken string `json:"instance_token"` - BootstrapAuthorizationToken string `json:"bootstrap_authorization_token"` - InitialRoot string `json:"initial_root"` - CleanSnapshot controllerArtifactPin `json:"clean_snapshot"` - OptionsArtifact controllerArtifactPin `json:"options_artifact"` - EngineConfiguration controllerEngineConfiguration `json:"engine_configuration"` - Disposable bool `json:"disposable"` - RequireInitiallyEmpty bool `json:"require_initially_empty"` - EmptyOperationLog bool `json:"empty_operation_log"` -} - -type controllerStopBinding struct { - BindingSHA256 string `json:"binding_sha256"` -} - -type controllerRequest struct { - SchemaVersion string `json:"schema_version"` - RequestID string `json:"request_id"` - Operation string `json:"operation"` - Start *controllerStartBinding `json:"start,omitempty"` - Stop *controllerStopBinding `json:"stop,omitempty"` -} - -type controllerResponse struct { - SchemaVersion string `json:"schema_version"` - RequestID string `json:"request_id"` - OK bool `json:"ok"` - CapabilityID string `json:"capability_id"` - State string `json:"state"` - BindingSHA256 string `json:"binding_sha256"` - DirectoryIdentity directoryIdentity `json:"directory_identity"` - Quiesced bool `json:"quiesced"` - Error *controllerResponseError `json:"error,omitempty"` -} - -type directoryIdentity struct { - Profile string `json:"profile"` - Device uint64 `json:"device"` - Inode uint64 `json:"inode"` -} - -func (identity directoryIdentity) validate() error { - if identity.Profile != directoryIdentityProfile || identity.Device == 0 || identity.Inode == 0 { - return errors.New("physical directory identity must bind a nonzero POSIX device/inode") - } - return nil -} - -type controllerResponseError struct { - Code string `json:"code"` - Message string `json:"message"` -} - -type fskvOptionsIdentity struct { - SchemaVersion string `json:"schema_version"` - Engine string `json:"engine"` - Module string `json:"module"` - Version string `json:"version"` - OptionsSource string `json:"options_source"` - DirectoryPolicy string `json:"directory_policy"` - ValueDirectoryPolicy string `json:"value_directory_policy"` - SyncWrites bool `json:"sync_writes"` - Logger string `json:"logger"` - WALPolicy string `json:"wal_policy"` - LSMPolicy string `json:"lsm_policy"` - CompactionPolicy string `json:"compaction_policy"` - GCPolicy string `json:"gc_policy"` - PrimaryStorageScope string `json:"primary_storage_scope"` - LookupIndexProfile string `json:"lookup_index_profile"` - LookupIndexSyncWrites bool `json:"lookup_index_sync_writes"` - LookupIndexLifecycle string `json:"lookup_index_lifecycle"` -} - -type selfTestGatewayController struct { - command *exec.Cmd - stdin io.WriteCloser - output *bufio.Scanner - stderr bytes.Buffer - bindingSHA256 string - directoryID directoryIdentity -} - -func hasMALTSelfTestFlag(arguments []string) bool { - for _, argument := range arguments { - if argument == "-self-test-corpus" || argument == "--self-test-corpus" || strings.HasPrefix(argument, "-self-test-corpus=") || strings.HasPrefix(argument, "--self-test-corpus=") { - return true - } - } - return false -} - -func parseMALTSelfTestFlags(arguments []string) (maltSelfTestConfig, error) { - flags := flag.NewFlagSet("malt-eval-rq3-malt-worker-self-test", flag.ContinueOnError) - flags.SetOutput(io.Discard) - corpus := flags.String("self-test-corpus", "", "formal E0 MALT adapter corpus") - controller := flags.String("gateway-controller", "", "pinned disposable Gateway controller") - clean := flags.String("clean-snapshot", "", "pinned empty Gateway snapshot descriptor") - options := flags.String("fskv-options", "", "pinned synchronous FSKV options descriptor") - timeout := flags.Duration("request-timeout", 2*time.Minute, "per-request self-test timeout") - var boundWorkloads, gitExecutables repeatedPathFlag - flags.Var(&boundWorkloads, "bound-workload", "production RQ3 workload artifact bound by formal E0 (repeatable)") - flags.Var(&gitExecutables, "git-executable", "production Git executable bound by formal E0 (repeatable)") - if err := flags.Parse(arguments); err != nil { - return maltSelfTestConfig{}, err - } - if flags.NArg() != 0 || *corpus == "" || *controller == "" || *clean == "" || *options == "" || *timeout <= 0 || *timeout > 10*time.Minute { - return maltSelfTestConfig{}, errors.New("formal E0 self-test requires -self-test-corpus, -gateway-controller, -clean-snapshot, -fskv-options, and a bounded positive -request-timeout") - } - for _, path := range []string{*corpus, *controller, *clean, *options} { - if !filepath.IsAbs(path) || filepath.Clean(path) != path { - return maltSelfTestConfig{}, errors.New("formal E0 self-test file paths must be absolute and clean") - } - } - return maltSelfTestConfig{ - corpusPath: *corpus, controllerPath: *controller, cleanPath: *clean, optionsPath: *options, requestTimeout: *timeout, - boundWorkloads: append([]string(nil), boundWorkloads...), gitExecutables: append([]string(nil), gitExecutables...), - }, nil -} - -func runMALTAdapterSelfTest(ctx context.Context, config maltSelfTestConfig, output io.Writer) error { - if err := validateMALTSelfTestInputs(config); err != nil { - return err - } - var corpus maltAdapterSelfTestCorpus - if err := decodeMALTSelfTestFile(config.corpusPath, maxWorkerLineBytes, &corpus); err != nil { - return fmt.Errorf("MALT adapter self-test corpus: %w", err) - } - if err := corpus.validate(); err != nil { - return err - } - cleanPin, err := describeControllerArtifact(config.cleanPath) - if err != nil { - return fmt.Errorf("clean snapshot input: %w", err) - } - optionsPin, err := describeControllerArtifact(config.optionsPath) - if err != nil { - return fmt.Errorf("FSKV options input: %w", err) - } - var options fskvOptionsIdentity - if err := decodeMALTSelfTestFile(config.optionsPath, 1<<20, &options); err != nil { - return fmt.Errorf("FSKV options identity: %w", err) - } - if options.SchemaVersion != "gateway-fskv-options/v3" || options.Engine != "fs" || options.Module != "gateway/internal/kv/fs" || options.Version != "v3" || - options.OptionsSource != "fs.NewEvaluation" || options.DirectoryPolicy != "campaign-empty-directory" || options.ValueDirectoryPolicy != "immutable-content-addressed-objects" || !options.SyncWrites || options.Logger != "not-applicable" || - options.WALPolicy != "none" || options.LSMPolicy != "none" || options.CompactionPolicy != "none" || options.GCPolicy != "strict-orphan-reclamation-on-open-and-quiesce" || - options.PrimaryStorageScope != "head-generations-and-content-addressed-objects/v1" || options.LookupIndexProfile != healthFlatLookupIndex || options.LookupIndexSyncWrites || options.LookupIndexLifecycle != "rebuild-on-open-remove-on-close/v1" { - return errors.New("FSKV options input has an invalid identity") - } - - results := make([]e0selftest.CaseResult, 0, len(corpus.PositiveCases)+len(corpus.HostileCases)) - for _, testCase := range corpus.PositiveCases { - if err := executeMALTPositiveCase(ctx, config, cleanPin, optionsPin, options.Version, testCase); err != nil { - return fmt.Errorf("MALT adapter self-test positive case %q: %w", testCase.ID, err) - } - results = append(results, e0selftest.CaseResult{ID: testCase.ID, Passed: true}) - } - for _, testCase := range corpus.HostileCases { - if err := executeMALTHostileCase(ctx, config, cleanPin, optionsPin, options.Version, testCase, corpus.PositiveCases); err != nil { - return fmt.Errorf("MALT adapter self-test hostile case %q: %w", testCase.ID, err) - } - results = append(results, e0selftest.CaseResult{ID: testCase.ID, Passed: true}) - } - receipt, err := e0selftest.Issue("rq3.malt-system-adapter", maltAdapterSelfTestProfile, results) - if err != nil { - return err - } - encoder := json.NewEncoder(output) - encoder.SetEscapeHTML(false) - return encoder.Encode(receipt) -} - -func (c maltAdapterSelfTestCorpus) validate() error { - if c.SchemaVersion != maltAdapterSelfTestCorpusSchema || len(c.PositiveCases) != len(maltAdapterSelfTestProfile.PositiveCases) || len(c.HostileCases) != len(maltAdapterSelfTestProfile.HostileCases) { - return errors.New("MALT adapter self-test corpus has an invalid schema or case count") - } - for index, testCase := range c.PositiveCases { - if testCase.ID != maltAdapterSelfTestProfile.PositiveCases[index] || testCase.Run.Workload.Kind != testCase.SourceKind || testCase.Run.PassMode != "accounting" || testCase.Run.RunPhase != "accounting" || testCase.Run.RunIndex != 0 || testCase.Run.InitialRoot != "" { - return fmt.Errorf("MALT adapter positive case %d does not match the compiled source/run contract", index) - } - if index == 0 && testCase.SourceKind != "controlled" || index == 1 && testCase.SourceKind != "git-first-parent" { - return fmt.Errorf("MALT adapter positive case %q has a mismatched source kind", testCase.ID) - } - if err := validateWorkloadIdentity(testCase.Run.Workload); err != nil { - return fmt.Errorf("MALT adapter positive case %q identity: %w", testCase.ID, err) - } - if err := validateFrozenCommitListBinding(testCase.Run); err != nil { - return fmt.Errorf("MALT adapter positive case %q commit-list binding: %w", testCase.ID, err) - } - } - wantKinds := []string{"malformed-request", "tampered-payload", "stale-before-value", "commit-list-digest-mismatch", "commit-manifest-before-gateway-side-effects", "valid-wrong-gateway-root", "unfinished-stream", "failed-stream-reuse", "active-stream-binding-reuse"} - for index, testCase := range c.HostileCases { - if testCase.ID != maltAdapterSelfTestProfile.HostileCases[index] || testCase.Kind != wantKinds[index] { - return fmt.Errorf("MALT adapter hostile case %d does not match the compiled contract", index) - } - } - return nil -} - -func executeMALTPositiveCase(ctx context.Context, config maltSelfTestConfig, cleanPin, optionsPin controllerArtifactPin, version string, testCase maltAdapterSelfTestPositiveCase) error { - return withDisposableSelfTestGateway(ctx, config, cleanPin, optionsPin, version, testCase.ID, testCase.Run, func(worker *campaignWorker) error { - requests := maltStreamRequests(testCase.ID, testCase.Run) - responses, err := runMALTSelfTestWorker(ctx, worker, requests) - if err != nil { - return err - } - wantCapability := maltAdapterV8ExpectedCapability() - if len(responses) != len(requests) { - return fmt.Errorf("production MALT streaming worker returned %d envelopes, want %d", len(responses), len(requests)) - } - for _, response := range responses { - if !response.OK { - return fmt.Errorf("production MALT request %s failed: %+v", response.RequestID, response.Error) - } - } - if responses[0].Capability == nil || !reflect.DeepEqual(*responses[0].Capability, wantCapability) { - return fmt.Errorf("production MALT capability differs from the current E0 contract: %+v", responses[0].Capability) - } - if responses[len(responses)-1].Stream == nil || !responses[len(responses)-1].Stream.Complete { - return errors.New("production MALT streaming worker did not finish its stream") - } - var records []commitRecord - writeEventsByCommit := make(map[string]int) - writeEvents := 0 - for index, response := range responses[1 : len(responses)-1] { - wantScope := resultScopeStreamChunk - if index == 0 { - wantScope = resultScopeStreamStart - } - if !response.OK || response.Result == nil || response.Result.CapabilityID != capabilityID || response.Result.System != systemMALTFlat || response.Result.ResultScope != wantScope || !reflect.DeepEqual(response.Result.Workload, testCase.Run.Workload) { - return fmt.Errorf("production MALT stream response is incomplete: %#v", response) - } - records = append(records, response.Result.Commits...) - writeEvents += len(response.Result.WriteEvents) - for _, event := range response.Result.WriteEvents { - writeEventsByCommit[event.CommitID]++ - } - } - if len(records) != len(testCase.Run.Commits)+1 || writeEvents == 0 || responses[len(responses)-1].Stream.CommitsApplied != uint32(len(records)) { - return errors.New("production MALT streaming worker returned an incomplete exact-ledger result") - } - for index, record := range records { - want := testCase.Run.Snapshot.CommitID - if index > 0 { - want = testCase.Run.Commits[index-1].CommitID - } - if record.CommitID != want || record.Root == "" || record.HistoryRootsRetained != uint32(index+1) || !record.OracleUnmeasured || - record.NonWorkloadSetupRootsRetained != 0 { - return fmt.Errorf("MALT commit record %d does not bind the retained history", index) - } - if index > 0 && len(testCase.Run.Commits[index-1].Mutations) == 0 && - (record.Root != record.ParentRoot || record.LogicalObjectsChanged != 0 || record.LogicalBindingsChanged != 0 || record.LogicalPayloadBytes != 0 || record.AdapterPayloadInputBytes != 0 || writeEventsByCommit[record.CommitID] != 0) { - return fmt.Errorf("MALT no-op commit %q changed the root or emitted writes", want) - } - } - return nil - }) -} - -// maltAdapterV8ExpectedCapability is deliberately independent of -// supportedCapability. These literals freeze the E0 v8 contract so a -// production layout, commitment, persistence, or accounting change cannot -// silently update both the implementation and its expected self-test value. -func maltAdapterV8ExpectedCapability() capability { - return capability{ - SchemaVersion: "malt-rq3-malt-boundary-capability/v4", - CapabilityID: "rq3.malt-flat-kzg-fskv-arcset.v7", - LayoutProfile: "malt-flat-canonical-path-map/v1", - CommitmentBackend: "kzg", - KVBackend: "fs", - ArcTablePersistence: "fskv-incremental-generations-bounded-index/v2", - CheckpointEnabled: false, - MaterializationCache: "none", - CASRoleIndex: "process-temporary-leveldb-bounded-memory-unmeasured/v1", - System: "malt-flat", - Boundary: []string{ - "MALT-flat one-map canonical-path to whole-file-CID layout; KZG is fixed as a control and is not an independent evaluation variable", - "Gateway controller-owned filesystem CAS exact batch dispositions; payload bytes and product ACL/quota metadata remain outside measured FSKV", - "Gateway primary FSKV stores and accounts canonical ArcSet key-plus-value bytes; its bounded process-temporary lookup index is rebuildable and unmeasured; checkpoints disabled; no materialization cache", - "Worker cross-category CAS validation uses bounded-memory LevelDB; temporary disk grows O(unique CIDs), is removed on close, and its storage and I/O time are unmeasured", - }, - Supported: true, - ExactCategories: []string{ - "arctable-arcset-records", - "arctable-lineage-metadata", - "cas-structural-metadata", - "logical-changed-payload", - "root-version-metadata", - }, - AttemptedVsPersisted: true, - ReplacementByteFlow: true, - SameValueAttempts: true, - OneRootPerCommit: true, - HistoryRetention: "all-roots", - GatewayAccountingProfile: "gateway.authentication-write-accounting/0", - GatewayByteMethod: "durable-kv-key-plus-value-bytes/v2", - AggregateKeyBinding: "object-ledger-sha256/category/disposition/v1", - DeleteLifecycle: true, - MissingCategories: []string{}, - MissingMetrics: []string{}, - GapsFailClosed: true, - MaximumWholeFileBytes: 67_108_864, - MaximumSnapshotFiles: 32_767, - MaximumMutationsPerChunk: 32_768, - MaximumFlatPrefixChanges: 65_536, - } -} - -func maltStreamRequests(prefix string, run runSpec) []workerRequest { - requests := []workerRequest{{SchemaVersion: workerRequestSchema, RequestID: prefix + "-cap", Operation: operationCapabilities}} - start := run - start.Commits = []rq3baseline.Commit{} - start.CommitManifest = make([]string, 0, len(run.Commits)+1) - start.CommitManifest = append(start.CommitManifest, run.Snapshot.CommitID) - for _, commit := range run.Commits { - start.CommitManifest = append(start.CommitManifest, commit.CommitID) - } - requests = append(requests, workerRequest{SchemaVersion: workerRequestSchema, RequestID: prefix + "-start", Operation: operationStreamStart, Run: &start}) - for index, commit := range run.Commits { - chunk := runSpec{PassMode: run.PassMode, RunPhase: run.RunPhase, ClusterID: run.ClusterID, RunIndex: run.RunIndex, Workload: run.Workload, Snapshot: rq3baseline.Snapshot{}, Commits: []rq3baseline.Commit{commit}} - requests = append(requests, workerRequest{SchemaVersion: workerRequestSchema, RequestID: fmt.Sprintf("%s-chunk-%d", prefix, index), Operation: operationStreamChunk, Run: &chunk}) - } - return append(requests, workerRequest{SchemaVersion: workerRequestSchema, RequestID: prefix + "-finish", Operation: operationStreamFinish}) -} - -func bindMALTSelfTestCommitList(run *runSpec) error { - commitIDs := make([]string, 0, len(run.Commits)+1) - commitIDs = append(commitIDs, run.Snapshot.CommitID) - for _, commit := range run.Commits { - commitIDs = append(commitIDs, commit.CommitID) - } - encoded, err := json.Marshal(struct { - Commits []string `json:"commits"` - }{Commits: commitIDs}) - if err != nil { - return err - } - digest := sha256.Sum256(encoded) - run.Workload.CommitListSHA256 = hex.EncodeToString(digest[:]) - return nil -} - -func runMALTSelfTestWorker(ctx context.Context, worker *campaignWorker, requests []workerRequest) ([]workerResponse, error) { - var input bytes.Buffer - for _, request := range requests { - encoded, err := json.Marshal(request) - if err != nil { - return nil, err - } - input.Write(encoded) - input.WriteByte('\n') - } - var output bytes.Buffer - runErr := runWorker(ctx, &input, &output, worker) - decoder := json.NewDecoder(&output) - responses := make([]workerResponse, 0, len(requests)) - for { - var response workerResponse - if err := decoder.Decode(&response); errors.Is(err, io.EOF) { - break - } else if err != nil { - return nil, errors.Join(runErr, err) - } - responses = append(responses, response) - } - return responses, runErr -} - -func executeMALTHostileCase(ctx context.Context, config maltSelfTestConfig, cleanPin, optionsPin controllerArtifactPin, version string, testCase maltAdapterSelfTestHostileCase, positives []maltAdapterSelfTestPositiveCase) error { - switch testCase.Kind { - case "malformed-request": - if _, err := decodeWorkerRequest([]byte(`{"schema_version":"malt-rq3-malt-worker-request/v2","request_id":"malformed","operation":"run","unknown":true}`)); err == nil { - return errors.New("production MALT request decoder accepted an unknown field") - } - return nil - case "tampered-payload": - run, err := cloneMALTSelfTestRun(positives[0].Run) - if err != nil { - return err - } - run.Snapshot.Files[0].PayloadBase64 = "dGFtcGVyZWQ=" - return withDisposableSelfTestGateway(ctx, config, cleanPin, optionsPin, version, testCase.ID, run, func(worker *campaignWorker) error { - requests := maltStreamRequests(testCase.ID, run)[:2] - responses, runErr := runMALTSelfTestWorker(ctx, worker, requests) - if runErr != nil || len(responses) != 2 || responses[1].OK || responses[1].Error == nil || !strings.Contains(strings.ToLower(responses[1].Error.Message), "payload sha-256 mismatch") { - return fmt.Errorf("production MALT stream did not reject a tampered payload binding: responses=%#v err=%v", responses, runErr) - } - return nil - }) - case "stale-before-value": - run, err := cloneMALTSelfTestRun(positives[0].Run) - if err != nil { - return err - } - run.Commits[0].Mutations[0].ExpectedOldSHA256 = strings.Repeat("0", 64) - return withDisposableSelfTestGateway(ctx, config, cleanPin, optionsPin, version, testCase.ID, run, func(worker *campaignWorker) error { - requests := maltStreamRequests(testCase.ID, run)[:3] - responses, runErr := runMALTSelfTestWorker(ctx, worker, requests) - if runErr != nil || len(responses) != 3 || responses[2].OK || responses[2].Error == nil || !strings.Contains(strings.ToLower(responses[2].Error.Message), "old payload digest mismatch") { - return fmt.Errorf("production MALT stream did not reject a stale expected-old value: responses=%#v err=%v", responses, runErr) - } - return nil - }) - case "commit-list-digest-mismatch": - run, err := cloneMALTSelfTestRun(positives[1].Run) - if err != nil { - return err - } - run.Workload.CommitListSHA256 = strings.Repeat("0", 64) - return withDisposableSelfTestGateway(ctx, config, cleanPin, optionsPin, version, testCase.ID, run, func(worker *campaignWorker) error { - requests := maltStreamRequests(testCase.ID, run) - responses, runErr := runMALTSelfTestWorker(ctx, worker, requests) - if len(responses) == 0 { - return fmt.Errorf("production MALT stream returned no response for a mismatched commit-list digest: err=%v", runErr) - } - if runErr != nil || len(responses) != len(requests) || responses[1].OK || responses[1].Error == nil || !strings.Contains(responses[1].Error.Message, "commit_list_sha256") { - return fmt.Errorf("production MALT stream accepted a mismatched commit-list digest: responses=%#v err=%v", responses, runErr) - } - for _, response := range responses[2:] { - if response.OK || response.Error == nil || !strings.Contains(response.Error.Message, "already complete") { - return fmt.Errorf("production MALT worker allowed reuse after rejecting a mismatched commit-list digest: responses=%#v", responses) - } - } - return nil - }) - case "commit-manifest-before-gateway-side-effects": - run, err := cloneMALTSelfTestRun(positives[0].Run) - if err != nil { - return err - } - return withDisposableSelfTestGateway(ctx, config, cleanPin, optionsPin, version, testCase.ID, run, func(worker *campaignWorker) error { - observed := &observingEvaluationGateway{base: worker.evaluation} - worker.evaluation = observed - requests := maltStreamRequests(testCase.ID, run)[:2] - requests[1].Run.CommitManifest[1] = "commit-not-bound-by-the-frozen-digest" - responses, runErr := runMALTSelfTestWorker(ctx, worker, requests) - if runErr != nil || len(responses) != 2 || !responses[0].OK || responses[1].OK || responses[1].Error == nil || !strings.Contains(responses[1].Error.Message, "commit_list_sha256") { - return fmt.Errorf("production MALT stream accepted an invalid preflight commit manifest: responses=%#v err=%v", responses, runErr) - } - if observed.healthCalls != 1 || observed.flatPrefixCalls != 0 { - return fmt.Errorf("invalid commit manifest crossed the Gateway side-effect boundary: health=%d flat-prefix=%d", observed.healthCalls, observed.flatPrefixCalls) - } - return nil - }) - case "valid-wrong-gateway-root": - run, err := cloneMALTSelfTestRun(positives[1].Run) - if err != nil { - return err - } - second := run.Commits[0].Mutations[0] - secondPayload := []byte("v3\n") - secondDigest := sha256.Sum256(secondPayload) - second.PayloadBase64 = "djMK" - second.PayloadSHA256 = hex.EncodeToString(secondDigest[:]) - second.ExpectedOldSHA256 = run.Commits[0].Mutations[0].PayloadSHA256 - run.Commits = append(run.Commits, rq3baseline.Commit{CommitID: "4444444444444444444444444444444444444444", Mutations: []rq3baseline.Mutation{second}}) - if err := bindMALTSelfTestCommitList(&run); err != nil { - return err - } - return withDisposableSelfTestGateway(ctx, config, cleanPin, optionsPin, version, testCase.ID, run, func(worker *campaignWorker) error { - observed := &observingEvaluationGateway{base: worker.evaluation, corruptFlatPrefixCall: 2} - worker.evaluation = observed - requests := maltStreamRequests(testCase.ID, run) - responses, runErr := runMALTSelfTestWorker(ctx, worker, requests) - if runErr != nil || len(responses) != len(requests) || !responses[0].OK || !responses[1].OK || responses[2].OK || responses[2].Error == nil || !strings.Contains(responses[2].Error.Message, "independent per-commit flat oracle") { - return fmt.Errorf("production MALT stream accepted a different valid Gateway root: responses=%#v err=%v", responses, runErr) - } - if observed.flatPrefixCalls != 2 { - return fmt.Errorf("worker performed Gateway mutations after the first wrong root: flat-prefix calls=%d", observed.flatPrefixCalls) - } - for _, response := range responses[3:] { - if response.OK || response.Error == nil || !strings.Contains(response.Error.Message, "already complete") { - return fmt.Errorf("worker did not terminate after the first wrong root: responses=%#v", responses) - } - } - return nil - }) - case "unfinished-stream": - run, err := cloneMALTSelfTestRun(positives[0].Run) - if err != nil { - return err - } - return withDisposableSelfTestGateway(ctx, config, cleanPin, optionsPin, version, testCase.ID, run, func(worker *campaignWorker) error { - responses, runErr := runMALTSelfTestWorker(ctx, worker, maltStreamRequests(testCase.ID, run)[:2]) - if runErr == nil || len(responses) != 2 || !strings.Contains(runErr.Error(), "before stream-finish") { - return fmt.Errorf("production MALT worker accepted EOF with an unfinished stream: responses=%#v err=%v", responses, runErr) - } - return nil - }) - case "failed-stream-reuse": - run, err := cloneMALTSelfTestRun(positives[0].Run) - if err != nil { - return err - } - run.Commits[0].Mutations[0].ExpectedOldSHA256 = strings.Repeat("0", 64) - return withDisposableSelfTestGateway(ctx, config, cleanPin, optionsPin, version, testCase.ID, run, func(worker *campaignWorker) error { - requests := maltStreamRequests(testCase.ID, run)[:3] - requests = append(requests, workerRequest{SchemaVersion: workerRequestSchema, RequestID: testCase.ID + "-reuse", Operation: operationStreamFinish}) - responses, runErr := runMALTSelfTestWorker(ctx, worker, requests) - if runErr != nil || len(responses) != 4 || responses[2].OK || responses[2].Error == nil || responses[3].OK || responses[3].Error == nil || !strings.Contains(responses[3].Error.Message, "already complete") { - return fmt.Errorf("production MALT worker allowed reuse after a failed stream: responses=%#v err=%v", responses, runErr) - } - return nil - }) - case "active-stream-binding-reuse": - run, err := cloneMALTSelfTestRun(positives[0].Run) - if err != nil { - return err - } - return withDisposableSelfTestGateway(ctx, config, cleanPin, optionsPin, version, testCase.ID, run, func(worker *campaignWorker) error { - requests := maltStreamRequests(testCase.ID, run)[:3] - requests[2].Run.PassMode = "timing" - requests = append(requests, workerRequest{SchemaVersion: workerRequestSchema, RequestID: testCase.ID + "-reuse", Operation: operationStreamFinish}) - responses, runErr := runMALTSelfTestWorker(ctx, worker, requests) - if runErr != nil || len(responses) != 4 || responses[2].OK || responses[2].Error == nil || responses[3].OK || responses[3].Error == nil || !strings.Contains(responses[3].Error.Message, "already complete") { - return fmt.Errorf("production MALT worker allowed reuse after an active-stream binding failure: responses=%#v err=%v", responses, runErr) - } - return nil - }) - default: - return fmt.Errorf("unsupported hostile kind %q", testCase.Kind) - } -} - -func validateMALTSelfTestInputs(config maltSelfTestConfig) error { - consumed := []string{config.corpusPath, config.controllerPath, config.cleanPath, config.optionsPath} - for _, path := range config.boundWorkloads { - if err := validateMALTBoundWorkload(path); err != nil { - return fmt.Errorf("bound workload %q: %w", path, err) - } - consumed = append(consumed, path) - } - for _, path := range config.gitExecutables { - if err := validateMALTBoundExecutable(path); err != nil { - return fmt.Errorf("Git executable %q: %w", path, err) - } - consumed = append(consumed, path) - } - for index, path := range consumed { - if !filepath.IsAbs(path) || filepath.Clean(path) != path { - return fmt.Errorf("consumed self-test path %d is not absolute and clean", index) - } - } - want, err := e0selftest.InputPaths() - if err != nil { - return err - } - slices.Sort(consumed) - if !slices.Equal(consumed, want) { - return fmt.Errorf("typed self-test flags consume %v, E0 invocation pins %v", consumed, want) - } - return nil -} - -func validateMALTBoundWorkload(path string) error { - data, err := os.ReadFile(path) - if err != nil { - return err - } - if len(data) == 0 || len(data) > maxWorkerLineBytes { - return errors.New("workload is empty or exceeds the bounded size") - } - var identity struct { - SchemaVersion string `json:"schema_version"` - } - decoder := json.NewDecoder(bytes.NewReader(data)) - if err := decoder.Decode(&identity); err != nil { - return err - } - if identity.SchemaVersion != "malt-rq3-controlled-workload/v1" && identity.SchemaVersion != "malt-eval-git-trace-request/v1" { - return fmt.Errorf("unsupported production workload schema %q", identity.SchemaVersion) - } - return nil -} - -func validateMALTBoundExecutable(path string) error { - info, err := os.Lstat(path) - if err != nil { - return err - } - if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() || info.Mode().Perm()&0o111 == 0 { - return errors.New("file must be a non-symlink executable") - } - return nil -} - -func withDisposableSelfTestGateway(ctx context.Context, config maltSelfTestConfig, cleanPin, optionsPin controllerArtifactPin, version, caseID string, run runSpec, execute func(*campaignWorker) error) (resultErr error) { - tempRoot, err := filepath.EvalSymlinks(os.TempDir()) - if err != nil { - return fmt.Errorf("resolve temporary directory: %w", err) - } - stateDirectory, err := os.MkdirTemp(tempRoot, "malt-rq3-e0-") - if err != nil { - return err - } - defer os.RemoveAll(stateDirectory) - directoryLease, err := openSelfTestDirectoryLease(stateDirectory) - if err != nil { - return err - } - defer directoryLease.Close() - if err := directoryLease.Verify("before Gateway controller start"); err != nil { - return err - } - baseURL, err := reserveSelfTestBaseURL() - if err != nil { - return err - } - instanceToken := selfTestToken("instance", caseID) - bootstrapToken := selfTestToken("bootstrap", caseID) - binding := controllerStartBinding{ - System: systemMALTFlat, WorkloadID: run.Workload.ID, PassMode: run.PassMode, RunPhase: run.RunPhase, - ClusterID: run.ClusterID, RunIndex: run.RunIndex, Directory: stateDirectory, DirectoryID: "e0-" + caseID, - DirectoryIdentity: directoryLease.Identity(), - BaseURL: baseURL, InstanceToken: instanceToken, BootstrapAuthorizationToken: bootstrapToken, - InitialRoot: "", CleanSnapshot: cleanPin, OptionsArtifact: optionsPin, - EngineConfiguration: controllerEngineConfiguration{ - Engine: "fs", Version: version, OptionsArtifactSHA256: optionsPin.SHA256, SyncWrites: true, - WALPolicy: "none", LSMPolicy: "none", - CompactionPolicy: "none", GCPolicy: "strict-orphan-reclamation-on-open-and-quiesce", - PrimaryStorageScope: "head-generations-and-content-addressed-objects/v1", - LookupIndexProfile: healthFlatLookupIndex, - }, - Disposable: true, RequireInitiallyEmpty: true, EmptyOperationLog: true, - } - controller, err := startSelfTestGatewayController(ctx, config.controllerPath, binding, directoryLease.Identity()) - if err != nil { - return err - } - defer func() { - if controller != nil { - if stopErr := controller.stop(); stopErr != nil && resultErr == nil { - resultErr = stopErr - } - } - }() - if err := directoryLease.Verify("after Gateway controller ready"); err != nil { - return err - } - worker, err := newCampaignWorker(workerConfig{ - gatewayBaseURL: baseURL, instanceToken: instanceToken, bootstrapAuthorizationToken: bootstrapToken, - requestTimeout: config.requestTimeout, - }) - if err != nil { - return err - } - executeErr := execute(worker) - afterWorkloadErr := directoryLease.Verify("after RQ3 workload") - stopErr := controller.stop() - controller = nil - afterStopErr := directoryLease.Verify("after Gateway controller stop") - return errors.Join(executeErr, afterWorkloadErr, stopErr, afterStopErr) -} - -func startSelfTestGatewayController(ctx context.Context, executable string, binding controllerStartBinding, leasedIdentity directoryIdentity) (*selfTestGatewayController, error) { - if err := validateRegisteredDirectoryIdentity(binding.DirectoryIdentity, leasedIdentity); err != nil { - return nil, err - } - command := exec.CommandContext(ctx, executable) - command.Env = []string{"LANG=C", "LC_ALL=C", "TZ=UTC"} - stdin, err := command.StdinPipe() - if err != nil { - return nil, err - } - stdout, err := command.StdoutPipe() - if err != nil { - _ = stdin.Close() - return nil, err - } - controller := &selfTestGatewayController{command: command, stdin: stdin} - controller.output = bufio.NewScanner(stdout) - controller.output.Buffer(make([]byte, 64<<10), maxWorkerLineBytes) - command.Stderr = &controller.stderr - if err := command.Start(); err != nil { - _ = stdin.Close() - return nil, err - } - fail := func(cause error) (*selfTestGatewayController, error) { - _ = stdin.Close() - if command.Process != nil { - _ = command.Process.Kill() - } - _ = command.Wait() - return nil, cause - } - bindingSHA256, err := canonicalSelfTestDigest(binding) - if err != nil { - return fail(err) - } - controller.bindingSHA256 = bindingSHA256 - controller.directoryID = leasedIdentity - request := controllerRequest{SchemaVersion: controllerRequestSchema, RequestID: "start", Operation: "start", Start: &binding} - if err := writeControllerRequest(stdin, request); err != nil { - return fail(err) - } - response, err := controller.readResponse() - if err != nil { - return fail(err) - } - if err := validateSelfTestGatewayControllerResponse(response, "start", "ready", bindingSHA256, leasedIdentity, false); err != nil { - return fail(err) - } - return controller, nil -} - -func (c *selfTestGatewayController) stop() error { - if c == nil || c.command == nil { - return errors.New("Gateway controller is not running") - } - request := controllerRequest{ - SchemaVersion: controllerRequestSchema, RequestID: "stop", Operation: "stop", - Stop: &controllerStopBinding{BindingSHA256: c.bindingSHA256}, - } - if err := writeControllerRequest(c.stdin, request); err != nil { - return c.abort(err) - } - response, err := c.readResponse() - if err != nil { - return c.abort(err) - } - if err := validateSelfTestGatewayControllerResponse(response, "stop", "stopped", c.bindingSHA256, c.directoryID, true); err != nil { - return c.abort(err) - } - if err := c.stdin.Close(); err != nil { - return c.abort(err) - } - if c.output.Scan() { - return c.abort(errors.New("Gateway controller emitted a trailing response")) - } - if err := c.output.Err(); err != nil { - return c.abort(err) - } - waitErr := c.command.Wait() - c.command = nil - if waitErr != nil { - return fmt.Errorf("Gateway controller exited unsuccessfully: %w; stderr=%q", waitErr, strings.TrimSpace(c.stderr.String())) - } - if stderr := strings.TrimSpace(c.stderr.String()); stderr != "" { - return fmt.Errorf("Gateway controller emitted unsolicited stderr: %q", stderr) - } - return nil -} - -func validateRegisteredDirectoryIdentity(registered, leased directoryIdentity) error { - if err := leased.validate(); err != nil { - return fmt.Errorf("leased physical directory identity: %w", err) - } - if err := registered.validate(); err != nil { - return fmt.Errorf("registered physical directory identity: %w", err) - } - if registered != leased { - return errors.New("registered physical directory identity does not match the opened directory lease") - } - return nil -} - -func validateSelfTestGatewayControllerResponse(response controllerResponse, requestID, state, bindingSHA256 string, identity directoryIdentity, quiesced bool) error { - if response.SchemaVersion != controllerResponseSchema || response.RequestID != requestID || !response.OK || - response.CapabilityID != controllerCapabilityID || response.State != state || response.BindingSHA256 != bindingSHA256 || - response.DirectoryIdentity != identity || response.Quiesced != quiesced || response.Error != nil { - return fmt.Errorf("Gateway controller returned an invalid %s response or directory identity: %#v", state, response) - } - return nil -} - -func (c *selfTestGatewayController) abort(cause error) error { - _ = c.stdin.Close() - if c.command != nil && c.command.Process != nil { - _ = c.command.Process.Kill() - } - if c.command != nil { - _ = c.command.Wait() - } - c.command = nil - return cause -} - -func (c *selfTestGatewayController) readResponse() (controllerResponse, error) { - if !c.output.Scan() { - if err := c.output.Err(); err != nil { - return controllerResponse{}, err - } - return controllerResponse{}, fmt.Errorf("Gateway controller closed before its response; stderr=%q", strings.TrimSpace(c.stderr.String())) - } - var response controllerResponse - if err := decodeStrictMALTSelfTestJSON(c.output.Bytes(), &response); err != nil { - return controllerResponse{}, err - } - return response, nil -} - -func writeControllerRequest(output io.Writer, request controllerRequest) error { - encoder := json.NewEncoder(output) - encoder.SetEscapeHTML(false) - return encoder.Encode(request) -} - -func reserveSelfTestBaseURL() (string, error) { - listener, err := net.Listen("tcp4", "127.0.0.1:0") - if err != nil { - return "", err - } - address := listener.Addr().String() - if err := listener.Close(); err != nil { - return "", err - } - return "http://" + address, nil -} - -func selfTestToken(purpose, caseID string) string { - digest := sha256.Sum256([]byte("malt-rq3-e0/" + purpose + "/" + caseID)) - return hex.EncodeToString(digest[:]) -} - -func describeControllerArtifact(path string) (controllerArtifactPin, error) { - info, err := os.Lstat(path) - if err != nil { - return controllerArtifactPin{}, err - } - if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() || info.Size() <= 0 || info.Size() > 1<<20 { - return controllerArtifactPin{}, errors.New("controller artifact must be a bounded non-symlink regular file") - } - data, err := os.ReadFile(path) - if err != nil { - return controllerArtifactPin{}, err - } - digest := sha256.Sum256(data) - return controllerArtifactPin{Path: path, SHA256: hex.EncodeToString(digest[:]), Bytes: info.Size()}, nil -} - -func cloneMALTSelfTestRun(run runSpec) (runSpec, error) { - encoded, err := json.Marshal(run) - if err != nil { - return runSpec{}, err - } - var clone runSpec - if err := json.Unmarshal(encoded, &clone); err != nil { - return runSpec{}, err - } - return clone, nil -} - -func decodeMALTSelfTestFile(path string, maximum int, target any) error { - data, err := os.ReadFile(path) - if err != nil { - return err - } - if len(data) == 0 || len(data) > maximum { - return errors.New("file is empty or exceeds the bounded size") - } - return decodeStrictMALTSelfTestJSON(data, target) -} - -func decodeStrictMALTSelfTestJSON(data []byte, target any) error { - if err := rejectDuplicateKeys(data); err != nil { - return err - } - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.DisallowUnknownFields() - if err := decoder.Decode(target); err != nil { - return err - } - var trailing json.RawMessage - if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { - if err == nil { - return errors.New("trailing JSON value") - } - return err - } - return nil -} - -func canonicalSelfTestDigest(value any) (string, error) { - encoded, err := json.Marshal(value) - if err != nil { - return "", err - } - var normalized any - decoder := json.NewDecoder(bytes.NewReader(encoded)) - decoder.UseNumber() - if err := decoder.Decode(&normalized); err != nil { - return "", err - } - encoded, err = json.Marshal(normalized) - if err != nil { - return "", err - } - digest := sha256.Sum256(encoded) - return hex.EncodeToString(digest[:]), nil -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/selftest_directory_linux.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/selftest_directory_linux.go deleted file mode 100644 index 75c49b0..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/selftest_directory_linux.go +++ /dev/null @@ -1,96 +0,0 @@ -//go:build linux - -package main - -import ( - "errors" - "fmt" - "os" - "syscall" -) - -// selfTestDirectoryLease keeps the exact temporary directory inode open for -// the complete controller-ready/workload/controller-stop lifecycle. The -// controller owns a separate descriptor; matching identities bind both -// processes to the same physical directory rather than merely the same path. -type selfTestDirectoryLease struct { - path string - directory *os.File - identity directoryIdentity -} - -func openSelfTestDirectoryLease(path string) (*selfTestDirectoryLease, error) { - pathInfo, err := os.Lstat(path) - if err != nil || pathInfo.Mode()&os.ModeSymlink != 0 || !pathInfo.IsDir() { - return nil, fmt.Errorf("self-test state path is not a real directory: %v", err) - } - directory, err := os.Open(path) - if err != nil { - return nil, fmt.Errorf("open self-test state directory: %w", err) - } - openedInfo, err := directory.Stat() - if err != nil || !openedInfo.IsDir() || !os.SameFile(pathInfo, openedInfo) { - _ = directory.Close() - return nil, fmt.Errorf("self-test state directory changed while opening: %v", err) - } - identity, err := identityFromOpenedDirectory(openedInfo) - if err != nil { - _ = directory.Close() - return nil, err - } - return &selfTestDirectoryLease{path: path, directory: directory, identity: identity}, nil -} - -func identityFromOpenedDirectory(info os.FileInfo) (directoryIdentity, error) { - stat, ok := info.Sys().(*syscall.Stat_t) - if !ok { - return directoryIdentity{}, errors.New("self-test directory stat does not expose Linux device/inode identity") - } - identity := directoryIdentity{Profile: directoryIdentityProfile, Device: uint64(stat.Dev), Inode: stat.Ino} - if err := identity.validate(); err != nil { - return directoryIdentity{}, err - } - return identity, nil -} - -func (lease *selfTestDirectoryLease) Identity() directoryIdentity { - if lease == nil { - return directoryIdentity{} - } - return lease.identity -} - -func (lease *selfTestDirectoryLease) Verify(boundary string) error { - if lease == nil || lease.directory == nil { - return errors.New("self-test directory lease is not open") - } - openedInfo, err := lease.directory.Stat() - if err != nil { - return fmt.Errorf("%s: stat opened self-test directory: %w", boundary, err) - } - openedIdentity, err := identityFromOpenedDirectory(openedInfo) - if err != nil { - return err - } - pathInfo, err := os.Lstat(lease.path) - if err != nil || pathInfo.Mode()&os.ModeSymlink != 0 || !pathInfo.IsDir() || !os.SameFile(openedInfo, pathInfo) { - return fmt.Errorf("%s: self-test directory path no longer names the leased inode: %v", boundary, err) - } - pathIdentity, err := identityFromOpenedDirectory(pathInfo) - if err != nil { - return err - } - if openedIdentity != lease.identity || pathIdentity != lease.identity { - return fmt.Errorf("%s: self-test directory device/inode changed", boundary) - } - return nil -} - -func (lease *selfTestDirectoryLease) Close() error { - if lease == nil || lease.directory == nil { - return nil - } - directory := lease.directory - lease.directory = nil - return directory.Close() -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/selftest_directory_linux_test.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/selftest_directory_linux_test.go deleted file mode 100644 index ed4c47c..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/selftest_directory_linux_test.go +++ /dev/null @@ -1,38 +0,0 @@ -//go:build linux - -package main - -import ( - "os" - "path/filepath" - "testing" -) - -func TestSelfTestDirectoryLeaseBindsOpenedInodeAcrossPathChecks(t *testing.T) { - parent := t.TempDir() - path := filepath.Join(parent, "state") - if err := os.Mkdir(path, 0o700); err != nil { - t.Fatal(err) - } - lease, err := openSelfTestDirectoryLease(path) - if err != nil { - t.Fatal(err) - } - defer lease.Close() - if err := lease.Identity().validate(); err != nil { - t.Fatal(err) - } - if err := lease.Verify("initial"); err != nil { - t.Fatal(err) - } - displaced := filepath.Join(parent, "displaced") - if err := os.Rename(path, displaced); err != nil { - t.Fatal(err) - } - if err := os.Mkdir(path, 0o700); err != nil { - t.Fatal(err) - } - if err := lease.Verify("rebound"); err == nil { - t.Fatal("path rebinding to a different directory inode was accepted") - } -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/selftest_directory_unsupported.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/selftest_directory_unsupported.go deleted file mode 100644 index 16214fc..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/selftest_directory_unsupported.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !linux - -package main - -import "errors" - -type selfTestDirectoryLease struct{} - -func openSelfTestDirectoryLease(string) (*selfTestDirectoryLease, error) { - return nil, errors.New("RQ3 physical directory identity requires Linux device/inode support") -} - -func (lease *selfTestDirectoryLease) Identity() directoryIdentity { return directoryIdentity{} } - -func (lease *selfTestDirectoryLease) Verify(string) error { - return errors.New("RQ3 physical directory identity requires Linux device/inode support") -} - -func (lease *selfTestDirectoryLease) Close() error { return nil } diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/types.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/types.go deleted file mode 100644 index 98689f3..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/types.go +++ /dev/null @@ -1,204 +0,0 @@ -package main - -import "github.com/dewebprotocol/malt-client/internal/evaluation/rq3baseline" - -const ( - workerRequestSchema = "malt-rq3-malt-worker-request/v2" - workerResponseSchema = "malt-rq3-malt-worker-response/v3" - capabilitySchema = "malt-rq3-malt-boundary-capability/v4" - runResultSchema = "malt-rq3-malt-run-result/v3" - capabilityID = "rq3.malt-flat-kzg-fskv-arcset.v7" - systemMALTFlat = "malt-flat" - operationCapabilities = "capabilities" - operationRun = "run" - operationStreamStart = "stream-start" - operationStreamChunk = "stream-chunk" - operationStreamFinish = "stream-finish" - resultScopeComplete = "complete-run" - resultScopeStreamStart = "stream-start" - resultScopeStreamChunk = "stream-chunk" - maxWorkerLineBytes = rq3baseline.MaximumJSONLRecordBytes - maxWorkerRequests = 4_096 - maximumMALTWholeFileBytes = 64 << 20 - maximumGatewayFlatPrefixChanges = 65_536 - maximumMALTSnapshotFiles = (maximumGatewayFlatPrefixChanges - 1) / 2 - maximumMALTChunkMutations = maximumGatewayFlatPrefixChanges / 2 - maximumMALTCommitManifest = 100_000 -) - -type workerRequest struct { - SchemaVersion string `json:"schema_version"` - RequestID string `json:"request_id"` - Operation string `json:"operation"` - Run *runSpec `json:"run,omitempty"` -} - -type runSpec struct { - PassMode string `json:"pass_mode"` - RunPhase string `json:"run_phase"` - ClusterID string `json:"cluster_id"` - RunIndex int `json:"run_index"` - Workload workloadIdentity `json:"workload"` - InitialRoot string `json:"initial_root,omitempty"` - Snapshot rq3baseline.Snapshot `json:"snapshot"` - Commits []rq3baseline.Commit `json:"commits"` - // CommitManifest is required only at stream-start. It binds the complete - // ordered history before the first Gateway mutation; chunks must then match - // it exactly and may not replace it. - CommitManifest []string `json:"commit_manifest,omitempty"` -} - -type workloadIdentity struct { - ID string `json:"id"` - Kind string `json:"kind"` - ArtifactSHA256 string `json:"artifact_sha256"` - SemanticSHA256 string `json:"semantic_sha256"` - ChunkBytes uint64 `json:"chunk_bytes"` - CommitListSHA256 string `json:"commit_list_sha256"` - HistoryRetention string `json:"history_retention"` - ControlledStructure string `json:"controlled_structure,omitempty"` - ControlledCoordinate *controlledCoordinate `json:"controlled_coordinate,omitempty"` -} - -type controlledCoordinate struct { - Operation string `json:"operation"` - PathDepth int `json:"path_depth"` - DirectoryWidth int `json:"directory_width"` - FileChunks int `json:"file_chunks"` - BatchSize int `json:"batch_size"` - RenamedBindings int `json:"renamed_bindings"` -} - -type workerResponse struct { - SchemaVersion string `json:"schema_version"` - RequestID string `json:"request_id"` - OK bool `json:"ok"` - Capability *capability `json:"capability,omitempty"` - Result *runResult `json:"result,omitempty"` - Error *workerError `json:"error,omitempty"` - Stream *streamStatus `json:"stream,omitempty"` -} - -type streamStatus struct { - Root string `json:"root"` - CommitsApplied uint32 `json:"commits_applied"` - Complete bool `json:"complete"` -} - -type workerError struct { - Code string `json:"code"` - Message string `json:"message"` - MissingCategories []string `json:"missing_categories,omitempty"` - MissingMetrics []string `json:"missing_metrics,omitempty"` -} - -type capability struct { - SchemaVersion string `json:"schema_version"` - CapabilityID string `json:"capability_id"` - LayoutProfile string `json:"layout_profile"` - CommitmentBackend string `json:"commitment_backend"` - KVBackend string `json:"kv_backend"` - ArcTablePersistence string `json:"arctable_persistence"` - CheckpointEnabled bool `json:"checkpoint_enabled"` - MaterializationCache string `json:"materialization_cache"` - CASRoleIndex string `json:"cas_role_index"` - System string `json:"system"` - Boundary []string `json:"boundary"` - Supported bool `json:"supported"` - ExactCategories []string `json:"exact_categories"` - AttemptedVsPersisted bool `json:"attempted_vs_persisted"` - ReplacementByteFlow bool `json:"replacement_byte_flow"` - SameValueAttempts bool `json:"same_value_attempts"` - OneRootPerCommit bool `json:"one_root_per_commit"` - HistoryRetention string `json:"history_retention"` - GatewayAccountingProfile string `json:"gateway_accounting_profile"` - GatewayByteMethod string `json:"gateway_byte_method"` - AggregateKeyBinding string `json:"aggregate_key_binding"` - DeleteLifecycle bool `json:"delete_lifecycle"` - MissingCategories []string `json:"missing_categories"` - MissingMetrics []string `json:"missing_metrics"` - GapsFailClosed bool `json:"gaps_fail_closed"` - MaximumWholeFileBytes int `json:"maximum_whole_file_bytes"` - MaximumSnapshotFiles int `json:"maximum_snapshot_files"` - MaximumMutationsPerChunk int `json:"maximum_mutations_per_chunk"` - MaximumFlatPrefixChanges int `json:"maximum_prefix_changes"` -} - -type runResult struct { - SchemaVersion string `json:"schema_version"` - CapabilityID string `json:"capability_id"` - System string `json:"system"` - ResultScope string `json:"result_scope"` - PassMode string `json:"pass_mode"` - RunPhase string `json:"run_phase"` - ClusterID string `json:"cluster_id"` - RunIndex int `json:"run_index"` - Workload workloadIdentity `json:"workload"` - Commits []commitRecord `json:"commits"` - WriteEvents []writeEvent `json:"write_events"` -} - -type commitRecord struct { - Order uint32 `json:"order"` - CommitID string `json:"commit_id"` - ParentRoot string `json:"parent_root,omitempty"` - Root string `json:"root"` - // HistoryRootsRetained counts workload roots only. The direct flat path has - // no non-workload setup root; the compatibility field is therefore zero. - HistoryRootsRetained uint32 `json:"history_roots_retained"` - NonWorkloadSetupRootsRetained uint32 `json:"non_workload_setup_roots_retained"` - LogicalObjectsChanged int `json:"logical_objects_changed"` - LogicalBindingsChanged int `json:"logical_bindings_changed"` - LogicalPayloadBytes int64 `json:"logical_payload_bytes"` - AdapterPayloadInputBytes int64 `json:"adapter_payload_input_bytes"` - // ClientComputeWallNS is retained for evaluator wire compatibility. It is - // the inclusive client-observed source-to-durable operation wall time, - // including payload upload and submission. Gateway replay/persist are - // nested diagnostics and must not be added to this total. - ClientComputeWallNS int64 `json:"client_compute_wall_ns"` - GatewayApplyAndStageWallNS int64 `json:"gateway_apply_and_stage_wall_ns"` - GatewayPersistWallNS int64 `json:"gateway_persist_wall_ns"` - OracleUnmeasured bool `json:"oracle_unmeasured"` -} - -type writeEvent struct { - Sequence uint64 `json:"sequence"` - CommitID string `json:"commit_id"` - Stage string `json:"stage"` - Category string `json:"category"` - Cause string `json:"cause"` - Disposition string `json:"disposition"` - ObjectKey string `json:"object_key"` - Count uint64 `json:"count"` - Bytes uint64 `json:"bytes"` - GrossNewBytes uint64 `json:"gross_new_bytes"` - ReclaimedBytes uint64 `json:"reclaimed_bytes"` - NetBytes int64 `json:"net_bytes"` - CASClassification string `json:"cas_classification"` -} - -func supportedCapability() capability { - return capability{ - SchemaVersion: capabilitySchema, CapabilityID: capabilityID, System: systemMALTFlat, - LayoutProfile: "malt-flat-canonical-path-map/v1", CommitmentBackend: "kzg", KVBackend: "fs", - ArcTablePersistence: "fskv-incremental-generations-bounded-index/v2", CheckpointEnabled: false, MaterializationCache: "none", CASRoleIndex: casRoleIndexProfile, - Boundary: []string{ - "MALT-flat one-map canonical-path to whole-file-CID layout; KZG is fixed as a control and is not an independent evaluation variable", - "Gateway controller-owned filesystem CAS exact batch dispositions; payload bytes and product ACL/quota metadata remain outside measured FSKV", - "Gateway primary FSKV stores and accounts canonical ArcSet key-plus-value bytes; its bounded process-temporary lookup index is rebuildable and unmeasured; checkpoints disabled; no materialization cache", - "Worker cross-category CAS validation uses bounded-memory LevelDB; temporary disk grows O(unique CIDs), is removed on close, and its storage and I/O time are unmeasured", - }, - Supported: true, - ExactCategories: []string{ - "arctable-arcset-records", "arctable-lineage-metadata", "cas-structural-metadata", "logical-changed-payload", "root-version-metadata", - }, - AttemptedVsPersisted: true, ReplacementByteFlow: true, SameValueAttempts: true, - OneRootPerCommit: true, HistoryRetention: "all-roots", - GatewayAccountingProfile: "gateway.authentication-write-accounting/0", - GatewayByteMethod: "durable-kv-key-plus-value-bytes/v2", - AggregateKeyBinding: "object-ledger-sha256/category/disposition/v1", DeleteLifecycle: true, - MissingCategories: []string{}, MissingMetrics: []string{}, GapsFailClosed: true, - MaximumWholeFileBytes: maximumMALTWholeFileBytes, MaximumSnapshotFiles: maximumMALTSnapshotFiles, - MaximumMutationsPerChunk: maximumMALTChunkMutations, MaximumFlatPrefixChanges: maximumGatewayFlatPrefixChanges, - } -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/worker.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/worker.go deleted file mode 100644 index b21ec99..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/worker.go +++ /dev/null @@ -1,268 +0,0 @@ -package main - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "math" - "net/http" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" - "github.com/dewebprotocol/malt-client/internal/evaluation/rq3baseline" - "github.com/dewebprotocol/malt-client/transport" -) - -const ( - healthCASAccounting = "cas.put-batch-disposition/v1" - healthCASIsolation = "gateway-process-serialized" - healthFlatLayout = "malt-flat/v1" - healthFlatStorageScope = "arctable-arcset-key-plus-value-only/v1" - healthFlatLookupIndex = "process-temporary-badger-rebuildable-unmeasured/v1" - healthCommitmentTreatment = "fixed-kzg-control-not-independent-variable/v1" - healthFlatFSKVMode = "evaluation-incremental-generations-bounded-index/v2" -) - -type workerConfig struct { - gatewayBaseURL string - instanceToken string - bootstrapAuthorizationToken string - requestTimeout time.Duration - initialRoot string -} - -type campaignWorker struct { - config workerConfig - remote *transport.Client - evaluation evaluationGateway -} - -type evaluationGateway interface { - Health(context.Context) (*gatewaytransport.Health, error) - ApplyEvaluationFlatPrefix(context.Context, string, gatewaytransport.FlatPrefixMutation) (gatewaytransport.FlatPrefixResult, error) -} - -func newCampaignWorker(config workerConfig) (*campaignWorker, error) { - evaluation, err := gatewaytransport.New(gatewaytransport.Options{ - BaseURL: config.gatewayBaseURL, InstanceToken: config.instanceToken, - HTTPClient: &http.Client{Timeout: config.requestTimeout}, - }) - if err != nil { - return nil, err - } - client, err := transport.New(transport.Options{ - BaseURL: config.gatewayBaseURL, - HTTPClient: evaluation.InstanceHTTPClient(), - }) - if err != nil { - return nil, err - } - return &campaignWorker{config: config, remote: client, evaluation: evaluation}, nil -} - -func (w *campaignWorker) validateHealth(ctx context.Context) error { - checkCtx, cancel := context.WithTimeout(ctx, w.config.requestTimeout) - defer cancel() - health, err := w.evaluation.Health(checkCtx) - if err != nil { - return fmt.Errorf("Gateway health: %w", err) - } - if health.Status != "ok" || health.EvaluationInstanceToken != w.config.instanceToken || - health.BlobBackend != "filesystem" || health.KVBackend != "fs" || health.ArcTableMode != "versioned" || - health.CommitmentProfile != "kzg" || health.CommitmentBackends != "ipa,kzg" || - health.EvaluationCASWriteAccounting != healthCASAccounting || health.EvaluationCASWriteIsolation != healthCASIsolation || - health.AuthenticationExactAcceptance != "false" || health.AuthenticationWriteAccounting != "" || - health.EvaluationAuthenticationBootstrap != "" || - health.EvaluationRQ3FlatPrefix != gatewaytransport.FlatPrefixProfile || - health.EvaluationRQ3FlatPrefixLayout != healthFlatLayout || - health.EvaluationRQ3FlatPrefixStorageScope != healthFlatStorageScope || - health.EvaluationRQ3FlatPrefixLookupIndex != healthFlatLookupIndex || - health.EvaluationRQ3FlatPrefixCommitmentTreatment != healthCommitmentTreatment || - health.EvaluationRQ3FlatPrefixFSKVMode != healthFlatFSKVMode || - health.EvaluationRQ3FlatPrefixCheckpoint != "false" || - health.EvaluationRQ3FlatPrefixMaterializationCache != "none" { - return fmt.Errorf("Gateway health does not expose the exact disposable FSKV-ArcTable/filesystem-CAS/KZG evaluation boundary") - } - return nil -} - -func (w *campaignWorker) run(ctx context.Context, spec runSpec) (*runResult, error) { - if !validRunCoordinate(spec.PassMode, spec.RunPhase, spec.ClusterID, spec.RunIndex) { - return nil, fmt.Errorf("invalid raw RQ3 pass/run/cluster coordinate") - } - if err := validateWorkloadIdentity(spec.Workload); err != nil { - return nil, err - } - if spec.CommitManifest != nil { - return nil, fmt.Errorf("one-shot run must not carry a stream commit manifest") - } - if err := validateFrozenCommitListBinding(spec); err != nil { - return nil, err - } - if w.config.initialRoot != spec.InitialRoot { - return nil, fmt.Errorf("-initial-root and run.initial_root do not match") - } - if spec.InitialRoot != "" { - return nil, fmt.Errorf("nonempty initial_root cannot fairly account the frozen snapshot; a clean disposable Gateway is required") - } - if err := prevalidateMALTFlatRun(spec); err != nil { - return nil, err - } - if err := w.validateHealth(ctx); err != nil { - return nil, err - } - return w.runFlatDirect(ctx, spec) -} - -// validateFrozenCommitListBinding closes the last source-to-worker identity -// hop. The evaluator derives this digest from the controlled workload or the -// exact Git first-parent trace; the worker independently recomputes it from -// the snapshot and commit sequence it is about to execute. -func validateFrozenCommitListBinding(spec runSpec) error { - commitIDs := make([]string, 0, len(spec.Commits)+1) - commitIDs = append(commitIDs, spec.Snapshot.CommitID) - for _, commit := range spec.Commits { - commitIDs = append(commitIDs, commit.CommitID) - } - return validateCommitManifest(spec.Workload, commitIDs) -} - -func validateCommitManifest(workload workloadIdentity, commitIDs []string) error { - if len(commitIDs) == 0 || len(commitIDs) > maximumMALTCommitManifest { - return fmt.Errorf("frozen commit manifest requires 1..%d entries", maximumMALTCommitManifest) - } - seen := make(map[string]struct{}, len(commitIDs)) - for index, commitID := range commitIDs { - if commitID == "" || len(commitID) > 256 { - return fmt.Errorf("frozen commit manifest entry %d is invalid", index) - } - if _, duplicate := seen[commitID]; duplicate { - return fmt.Errorf("frozen commit manifest repeats commit_id %q", commitID) - } - seen[commitID] = struct{}{} - } - encoded, err := json.Marshal(struct { - Commits []string `json:"commits"` - }{Commits: commitIDs}) - if err != nil { - return fmt.Errorf("encode frozen commit-list binding: %w", err) - } - digest := sha256.Sum256(encoded) - if hex.EncodeToString(digest[:]) != workload.CommitListSHA256 { - return fmt.Errorf("frozen snapshot/commit sequence does not match workload commit_list_sha256") - } - return nil -} - -type baselineRunner func(context.Context, rq3baseline.RunSpec) (*rq3baseline.RunResult, error) - -// postMeasurementBaselineConformance deliberately runs only after the MALT -// session has produced every commit and passed its retained-state audit. The -// UnixFS adapter is therefore never a one-sided prewarm for measured MALT. -func postMeasurementBaselineConformance(ctx context.Context, spec rq3baseline.RunSpec, result *runResult, runBaseline baselineRunner) error { - if result == nil || len(result.Commits) != len(spec.Commits)+1 { - return fmt.Errorf("MALT measurements are incomplete before baseline conformance") - } - baseline, err := runBaseline(ctx, spec) - if err != nil { - return fmt.Errorf("post-measurement frozen workload baseline conformance: %w", err) - } - if len(baseline.Records) != len(result.Commits) { - return fmt.Errorf("post-measurement baseline conformance returned a mismatched commit count") - } - for index, record := range baseline.Records { - commit := result.Commits[index] - if record.CommitID != commit.CommitID || record.LogicalObjectsChanged != commit.LogicalObjectsChanged || - record.LogicalBindingsChanged != commit.LogicalBindingsChanged || record.AdapterPayloadInputBytes != commit.AdapterPayloadInputBytes || - commit.HistoryRootsRetained != uint32(index+1) || commit.NonWorkloadSetupRootsRetained != 1 { - return fmt.Errorf("post-measurement baseline conformance mismatch at commit %q", commit.CommitID) - } - } - return nil -} - -func boolPointer(value bool) *bool { return &value } - -func validRunCoordinate(passMode, runPhase, clusterID string, runIndex int) bool { - if !canonicalEvaluatorID(clusterID) || runIndex < 0 { - return false - } - if passMode == "accounting" { - return runPhase == "accounting" && runIndex == 0 - } - return passMode == "timing" && (runPhase == "feasibility" || runPhase == "measured") -} - -func isControlledListHistory(spec runSpec) bool { - return spec.Workload.Kind == "controlled" && spec.Workload.ControlledStructure == "list" -} - -func validateWorkloadIdentity(value workloadIdentity) error { - if !canonicalEvaluatorID(value.ID) || (value.Kind != "controlled" && value.Kind != "git-first-parent") || - !canonicalSHA256(value.ArtifactSHA256) || !canonicalSHA256(value.SemanticSHA256) || - !canonicalSHA256(value.CommitListSHA256) || value.ChunkBytes == 0 || value.ChunkBytes > 16<<20 || - value.HistoryRetention != "all-roots" || (value.Kind == "controlled") != (value.ControlledCoordinate != nil) || - (value.Kind == "controlled" && value.ControlledStructure != "map" && value.ControlledStructure != "list") || - (value.Kind != "controlled" && value.ControlledStructure != "") || !validControlledCoordinate(value) { - return fmt.Errorf("invalid RQ3 workload identity") - } - return nil -} - -func validControlledCoordinate(value workloadIdentity) bool { - if value.ControlledCoordinate == nil { - return value.Kind != "controlled" - } - coordinate := value.ControlledCoordinate - isRelocation := coordinate.Operation == "rename" || coordinate.Operation == "move" - return value.Kind == "controlled" && canonicalEvaluatorID(coordinate.Operation) && coordinate.PathDepth > 0 && - coordinate.DirectoryWidth > 0 && coordinate.FileChunks > 0 && coordinate.BatchSize > 0 && coordinate.RenamedBindings >= 0 && - (coordinate.Operation == "subtree-rename") == (coordinate.RenamedBindings > 1) && - (!isRelocation || coordinate.RenamedBindings == 1) && - (isRelocation || coordinate.Operation == "subtree-rename" || coordinate.RenamedBindings == 0) -} - -func canonicalEvaluatorID(value string) bool { - if len(value) == 0 || len(value) > 128 || !lowerAlphaNumeric(value[0]) { - return false - } - for index := 1; index < len(value); index++ { - character := value[index] - if !lowerAlphaNumeric(character) && character != '.' && character != '_' && character != '-' { - return false - } - } - return true -} - -func lowerAlphaNumeric(value byte) bool { - return value >= 'a' && value <= 'z' || value >= '0' && value <= '9' -} - -func operationID(commitID string, order uint32) string { - digest := sha256.Sum256([]byte(fmt.Sprintf("%d\x00%s", order, commitID))) - return "rq3-" + hex.EncodeToString(digest[:16]) -} - -func durationNanos(value time.Duration) int64 { - if value <= 0 { - return 1 - } - return value.Nanoseconds() -} - -func addDuration(a int64, b uint64) (int64, error) { - if b > math.MaxInt64 || a > math.MaxInt64-int64(b) { - return 0, fmt.Errorf("client duration overflow") - } - return a + int64(b), nil -} - -func evaluatorNanos(value uint64) (int64, error) { - if value > math.MaxInt64 { - return 0, fmt.Errorf("duration exceeds evaluator range") - } - return int64(value), nil -} diff --git a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/worker_health_test.go b/tools/evaluation/cmd/malt-eval-rq3-malt-worker/worker_health_test.go deleted file mode 100644 index 3493528..0000000 --- a/tools/evaluation/cmd/malt-eval-rq3-malt-worker/worker_health_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package main - -import ( - "context" - "errors" - "strings" - "testing" - "time" - - "github.com/dewebprotocol/malt-client/internal/evaluation/gatewaytransport" -) - -func TestValidateHealthRequiresSeparatedFilesystemCAS(t *testing.T) { - token := strings.Repeat("a", 64) - health := gatewaytransport.Health{ - Status: "ok", EvaluationInstanceToken: token, - KVBackend: "fs", BlobBackend: "filesystem", ArcTableMode: "versioned", - CommitmentProfile: "kzg", CommitmentBackends: "ipa,kzg", - EvaluationCASWriteAccounting: healthCASAccounting, EvaluationCASWriteIsolation: healthCASIsolation, - AuthenticationExactAcceptance: "false", - EvaluationRQ3FlatPrefix: gatewaytransport.FlatPrefixProfile, - EvaluationRQ3FlatPrefixLayout: healthFlatLayout, - EvaluationRQ3FlatPrefixStorageScope: healthFlatStorageScope, - EvaluationRQ3FlatPrefixLookupIndex: healthFlatLookupIndex, - EvaluationRQ3FlatPrefixCommitmentTreatment: healthCommitmentTreatment, - EvaluationRQ3FlatPrefixFSKVMode: healthFlatFSKVMode, - EvaluationRQ3FlatPrefixCheckpoint: "false", EvaluationRQ3FlatPrefixMaterializationCache: "none", - } - worker := &campaignWorker{ - config: workerConfig{instanceToken: token, requestTimeout: time.Second}, - evaluation: fixedHealthGateway{health: &health}, - } - if err := worker.validateHealth(t.Context()); err != nil { - t.Fatal(err) - } - tests := []struct { - name string - mutate func(*gatewaytransport.Health) - }{ - {name: "embedded-payload-cas", mutate: func(value *gatewaytransport.Health) { value.BlobBackend = "embedded" }}, - {name: "exact-client-root-acceptance", mutate: func(value *gatewaytransport.Health) { value.AuthenticationExactAcceptance = "true" }}, - {name: "client-root-write-accounting", mutate: func(value *gatewaytransport.Health) { value.AuthenticationWriteAccounting = gatewayAccountingProfile }}, - {name: "bootstrap-capability", mutate: func(value *gatewaytransport.Health) { - value.EvaluationAuthenticationBootstrap = "gateway.evaluation-client-root-bootstrap-object/v1" - }}, - {name: "missing-flat-prefix", mutate: func(value *gatewaytransport.Health) { value.EvaluationRQ3FlatPrefix = "" }}, - {name: "wrong-layout", mutate: func(value *gatewaytransport.Health) { value.EvaluationRQ3FlatPrefixLayout = "malt-hamt/v1" }}, - {name: "unbounded-lookup-index", mutate: func(value *gatewaytransport.Health) { value.EvaluationRQ3FlatPrefixLookupIndex = "in-memory-unbounded" }}, - {name: "variable-commitment", mutate: func(value *gatewaytransport.Health) { value.EvaluationRQ3FlatPrefixCommitmentTreatment = "variable" }}, - {name: "legacy-fskv", mutate: func(value *gatewaytransport.Health) { - value.EvaluationRQ3FlatPrefixFSKVMode = "evaluation-incremental-generations/v1" - }}, - {name: "checkpoint-enabled", mutate: func(value *gatewaytransport.Health) { value.EvaluationRQ3FlatPrefixCheckpoint = "true" }}, - {name: "materialization-cache", mutate: func(value *gatewaytransport.Health) { value.EvaluationRQ3FlatPrefixMaterializationCache = "disk" }}, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - invalid := health - test.mutate(&invalid) - worker.evaluation = fixedHealthGateway{health: &invalid} - if err := worker.validateHealth(t.Context()); err == nil { - t.Fatal("worker accepted an invalid Gateway evaluation boundary") - } - }) - } -} - -type fixedHealthGateway struct { - health *gatewaytransport.Health -} - -func (g fixedHealthGateway) Health(context.Context) (*gatewaytransport.Health, error) { - if g.health == nil { - return nil, errors.New("health unavailable") - } - copy := *g.health - return ©, nil -} - -func (fixedHealthGateway) ApplyEvaluationFlatPrefix(context.Context, string, gatewaytransport.FlatPrefixMutation) (gatewaytransport.FlatPrefixResult, error) { - return gatewaytransport.FlatPrefixResult{}, errors.New("unexpected flat map") -} - -var _ evaluationGateway = fixedHealthGateway{}