L1: the integrity check reads the scroll (tablet 38)
L1Reader.verify_integrity/0 read the hash stored in integrity.sha256
and compared that string with the hash compiled into the module. It
never opened a verse. On that comparison of a constant with its own
copy rested `l1_hash: pass`, the Cloud's "L1 corrupted — the cloud is
not visible", and Construction §1.2. A changed verse would have passed
every one of them. Every existing test passed too, because every test
asked about an untouched L1.
- L1Reader.compute_tree_hash/1: SHA-256 over every file under torah/ —
relative path, then bytes — in the order scripts/build_l1.py hashed
them (directories by path, files by name, plain string order). On the
real L1 it yields the pinned 156b1d27…; for a tampered copy it yields
the same hash the Python implementation does.
- VavPipeline.L1Integrity (new, supervised): reads and hashes the scroll
when the pipeline starts (~0.9 s in the image) and again every ten
minutes; logs and signals the assembly when the answer changes.
- verify_integrity/0 now requires BOTH: the recorded hash is the pinned
one, AND the scroll on disk hashes to it (answered from the witness's
last reading; computed on the spot where the witness is not running).
Callers are unchanged — the Shema context, the health check and the
status endpoint all inherit the real check.
- scripts/verify_l1.py: the same reading by hand. KOHEN.md and
KOHEN_PROMPT.md used the hollow comparison as their L1 gate; they now
run this.
- test/tablet38_l1_integrity_test.exs — 9 tests: the hash and its
order; one byte / one added file / one rename each change it; the
witness; and the whole path — with an altered scroll and an untouched
integrity.sha256, verify_integrity fails, health says l1_hash: fail,
and /peah/ask does not answer.
Specs: architecture §2.2 (which had been made to confess the hollow
check in d7520cd) and Construction §1.2 now describe the real one — and
its limit: a change is noticed within ten minutes, not at the instant.
deploy/ARCHITECTURE.md likewise. Doorpost @expected_hash 7e5286b9… →
3f6d47a0…, in this commit, with the witnessed files that moved it
(architecture, vayakhel).
Read before committing: Elixir 483/0 on seeds 0 7 42 99 191243. The
built image, run twice: on the engraved scroll — "read and matches
(928ms)", l1_hash pass, /peah/ask 200; on a copy with ONE LETTER changed
in Bereishit 1:1 and integrity.sha256 untouched — L1_INTEGRITY_FAILED,
l1_hash fail, cloud not readable, /peah/ask 503.
Not done: the Haskell side. Luchot.Firmware.l1ExpectedHash is compared
with nothing; the engine reads L1 without hashing it (that would need a
SHA-256 dependency). The engine is reached only through the pipeline,
which now gates every cycle on the real check.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
d7520cd69d
commit
bbe2c4b7ba
11 files changed
+579
-17
No files matched your search
@@ -0,0 +1,247 @@
|
||||
defmodule VavPipeline.Tablet38L1IntegrityTest do
|
||||
@moduledoc """
|
||||
Tablet 38 (2026-10-04) — the integrity check did not check integrity.
|
||||
|
||||
`L1Reader.verify_integrity/0` compared the hash recorded in
|
||||
`integrity.sha256` with the hash compiled into the module. It never read
|
||||
a verse. Every test of it passed, because every test asked it about an
|
||||
untouched L1 — the one question a check that checks nothing answers
|
||||
correctly.
|
||||
|
||||
These tests change the scroll and ask again.
|
||||
"""
|
||||
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
import Plug.Test
|
||||
import Plug.Conn
|
||||
|
||||
alias VavPipeline.{HaltMonitor, L1Integrity, L1Reader, MannaLedger}
|
||||
alias VavPipeline.BigdeiKodesh.Router
|
||||
|
||||
@opts Router.init([])
|
||||
|
||||
# ============================================================
|
||||
# The hash itself
|
||||
# ============================================================
|
||||
describe "compute_tree_hash/1" do
|
||||
test "of the real scroll is the pinned hash, and the hash build_l1.py recorded" do
|
||||
assert {:ok, hash} = L1Reader.compute_tree_hash()
|
||||
assert hash == L1Reader.expected_hash()
|
||||
|
||||
recorded = L1Reader.l1_root() |> Path.join("integrity.sha256") |> File.read!() |> String.trim()
|
||||
assert hash == recorded
|
||||
end
|
||||
|
||||
test "hashes path then bytes, directories then files, in plain string order" do
|
||||
root = small_scroll()
|
||||
|
||||
# The order build_l1.py walks: the torah root's own files, then each
|
||||
# directory by path; within a directory, names as strings — so
|
||||
# 10.json comes before 2.json.
|
||||
expected =
|
||||
[
|
||||
{"index.json", "INDEX"},
|
||||
{"alef/1/1.json", "one"},
|
||||
{"alef/1/10.json", "ten"},
|
||||
{"alef/1/2.json", "two"},
|
||||
{"alef/2/1.json", "second chapter"}
|
||||
]
|
||||
|> Enum.reduce(:crypto.hash_init(:sha256), fn {rel, bytes}, ctx ->
|
||||
ctx |> :crypto.hash_update(rel) |> :crypto.hash_update(bytes)
|
||||
end)
|
||||
|> :crypto.hash_final()
|
||||
|> Base.encode16(case: :lower)
|
||||
|
||||
assert {:ok, ^expected} = L1Reader.compute_tree_hash(root)
|
||||
end
|
||||
|
||||
test "one changed byte, one added file, one renamed file — each changes the hash" do
|
||||
root = small_scroll()
|
||||
{:ok, original} = L1Reader.compute_tree_hash(root)
|
||||
verse = Path.join([root, "torah", "alef", "1", "2.json"])
|
||||
|
||||
File.write!(verse, "twO")
|
||||
assert {:ok, changed} = L1Reader.compute_tree_hash(root)
|
||||
assert changed != original
|
||||
|
||||
File.write!(verse, "two")
|
||||
assert {:ok, ^original} = L1Reader.compute_tree_hash(root)
|
||||
|
||||
File.write!(Path.join([root, "torah", "alef", "1", "3.json"]), "an added verse")
|
||||
assert {:ok, added} = L1Reader.compute_tree_hash(root)
|
||||
assert added != original
|
||||
File.rm!(Path.join([root, "torah", "alef", "1", "3.json"]))
|
||||
|
||||
File.rename!(verse, Path.join([root, "torah", "alef", "1", "20.json"]))
|
||||
assert {:ok, renamed} = L1Reader.compute_tree_hash(root)
|
||||
assert renamed != original
|
||||
end
|
||||
|
||||
test "a scroll that is not there is an error, not a hash" do
|
||||
assert {:error, reason} = L1Reader.compute_tree_hash("/nonexistent/l1")
|
||||
assert reason =~ "not a directory"
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# The witness
|
||||
# ============================================================
|
||||
describe "L1Integrity" do
|
||||
test "reads the scroll when it starts, and again when asked" do
|
||||
root = small_scroll()
|
||||
{:ok, pinned} = L1Reader.compute_tree_hash(root)
|
||||
|
||||
{:ok, pid} =
|
||||
L1Integrity.start_link(name: :tablet38_witness, l1_root: root, expected_hash: pinned, interval: :never)
|
||||
|
||||
on_exit(fn -> if Process.alive?(pid), do: GenServer.stop(pid) end)
|
||||
|
||||
assert :ok = L1Integrity.status(:tablet38_witness)
|
||||
assert %{computed: ^pinned, expected: ^pinned} = L1Integrity.reading(:tablet38_witness)
|
||||
|
||||
# Someone alters a verse. The last reading still says tov — it is a
|
||||
# reading, not a guard — until the scroll is read again.
|
||||
File.write!(Path.join([root, "torah", "alef", "1", "1.json"]), "0ne")
|
||||
assert {:error, reason} = L1Integrity.recheck(:tablet38_witness)
|
||||
assert reason =~ "L1_INTEGRITY_FAILED"
|
||||
assert reason =~ "expected #{pinned}"
|
||||
assert {:error, _} = L1Integrity.status(:tablet38_witness)
|
||||
|
||||
# Restored, it is tov again.
|
||||
File.write!(Path.join([root, "torah", "alef", "1", "1.json"]), "one")
|
||||
assert :ok = L1Integrity.recheck(:tablet38_witness)
|
||||
end
|
||||
|
||||
test "re-reads on its own at the interval" do
|
||||
root = small_scroll()
|
||||
{:ok, pinned} = L1Reader.compute_tree_hash(root)
|
||||
|
||||
{:ok, pid} =
|
||||
L1Integrity.start_link(name: :tablet38_periodic, l1_root: root, expected_hash: pinned, interval: 50)
|
||||
|
||||
on_exit(fn -> if Process.alive?(pid), do: GenServer.stop(pid) end)
|
||||
|
||||
assert :ok = L1Integrity.status(:tablet38_periodic)
|
||||
File.write!(Path.join([root, "torah", "index.json"]), "INDEX, altered")
|
||||
|
||||
assert eventually(fn -> match?({:error, _}, L1Integrity.status(:tablet38_periodic)) end)
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# The whole path: an altered scroll stops the vessel
|
||||
# ============================================================
|
||||
describe "with a scroll that does not match its pin" do
|
||||
setup do
|
||||
if HaltMonitor.halted?(), do: HaltMonitor.resolve("tablet38 test setup")
|
||||
MannaLedger.reset()
|
||||
write_provision(20)
|
||||
|
||||
# Sanity: with the real scroll, all is well.
|
||||
assert :ok = L1Reader.verify_integrity()
|
||||
|
||||
# Replace the supervised witness with one that reads an altered
|
||||
# copy — the recorded hash file is untouched and still "matches",
|
||||
# which is exactly the case the old check could not see.
|
||||
root = small_scroll()
|
||||
:ok = Supervisor.terminate_child(VavPipeline.Supervisor, L1Integrity)
|
||||
|
||||
{:ok, pid} =
|
||||
L1Integrity.start_link(l1_root: root, expected_hash: L1Reader.expected_hash(), interval: :never)
|
||||
|
||||
on_exit(fn ->
|
||||
if Process.alive?(pid), do: GenServer.stop(pid)
|
||||
{:ok, _} = Supervisor.restart_child(VavPipeline.Supervisor, L1Integrity)
|
||||
MannaLedger.reset()
|
||||
File.rm(VavPipeline.Manna.provision_file())
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "verify_integrity fails, though integrity.sha256 still holds the pinned hash" do
|
||||
recorded = L1Reader.l1_root() |> Path.join("integrity.sha256") |> File.read!() |> String.trim()
|
||||
assert recorded == L1Reader.expected_hash()
|
||||
|
||||
assert {:error, reason} = L1Reader.verify_integrity()
|
||||
assert reason =~ "L1_INTEGRITY_FAILED"
|
||||
assert reason =~ "the scroll on disk hashes to"
|
||||
end
|
||||
|
||||
test "health says l1_hash: fail and the vessel is not ready" do
|
||||
conn = conn(:get, "/kodesh/health") |> Router.call(@opts)
|
||||
body = Jason.decode!(conn.resp_body)
|
||||
|
||||
assert conn.status == 503
|
||||
assert body["checks"]["l1_hash"] == "fail"
|
||||
end
|
||||
|
||||
test "no cycle runs on it — /peah/ask does not answer from an altered scroll" do
|
||||
conn =
|
||||
conn(:post, "/peah/ask", Jason.encode!(%{question: "בראשית ברא אלהים"}))
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> Router.call(@opts)
|
||||
|
||||
refute conn.status == 200
|
||||
refute Map.has_key?(Jason.decode!(conn.resp_body), "answer")
|
||||
end
|
||||
end
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------
|
||||
|
||||
defp small_scroll do
|
||||
root = Path.join(System.tmp_dir!(), "tablet38_#{System.unique_integer([:positive])}")
|
||||
on_exit(fn -> File.rm_rf!(root) end)
|
||||
|
||||
files = %{
|
||||
"index.json" => "INDEX",
|
||||
"alef/1/1.json" => "one",
|
||||
"alef/1/2.json" => "two",
|
||||
"alef/1/10.json" => "ten",
|
||||
"alef/2/1.json" => "second chapter"
|
||||
}
|
||||
|
||||
for {rel, bytes} <- files do
|
||||
path = Path.join([root, "torah", rel])
|
||||
File.mkdir_p!(Path.dirname(path))
|
||||
File.write!(path, bytes)
|
||||
end
|
||||
|
||||
root
|
||||
end
|
||||
|
||||
defp eventually(fun, tries \\ 100) do
|
||||
cond do
|
||||
fun.() -> true
|
||||
tries == 0 -> false
|
||||
true -> Process.sleep(20) && eventually(fun, tries - 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp write_provision(compute_units) do
|
||||
today = Date.utc_today() |> Date.to_iso8601()
|
||||
expires = DateTime.utc_now() |> DateTime.add(6 * 3600) |> DateTime.to_iso8601()
|
||||
|
||||
provision = %{
|
||||
"date" => today,
|
||||
"provision" => %{
|
||||
"compute_units" => compute_units,
|
||||
"memory_mb" => 512,
|
||||
"api_calls_external" => 5,
|
||||
"storage_delta_mb" => 2,
|
||||
"sanhedrin_panels_max" => 2
|
||||
},
|
||||
"type" => "regular",
|
||||
"expires_at" => expires,
|
||||
"written_by" => "tablet38_test",
|
||||
"written_at" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}
|
||||
|
||||
path = VavPipeline.Manna.provision_file()
|
||||
File.mkdir_p!(Path.dirname(path))
|
||||
File.write!(path, Jason.encode!(provision))
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user