the ledger: a caller's own bedikot, read back and signed
The examination is the product; the record of it is what a caller can carry away. Every bedikah was already engraved in L3 — output hash and length, source digests, coupling trace, claims, violations, verdict, never the text — and no caller could get theirs back. - bedikot.key_id (migration): a SHA-256 digest of the courtyard key that brought the offering, never the key; threaded as :key_id through Bedikah.examine from every site that goes through the gate (courtyard bedikah, pair, the gate's pass and stream). Unique index on bedikah_ref. - VavPipeline.Ledger: entry (the examination's fields, none of the database's, not the owner's), canonical JSON (keys sorted at every level, no whitespace, UTF-8), Ed25519 sign/verify, public key, render. Seed from BIKORET_LEDGER_SIGNING_KEY (base64 of 32 bytes) in shorashim-secrets; absent → unsigned and says so. - GET /courtyard/ledger?limit&before — this key's bedikot, newest first, each rendered and signed; GET /courtyard/bedikah/<ref> — one, 404 for another key's or none alike; GET /peah/ledger_key — the public key. - Tamid §1.1 "The ledger" (Doorpost pin moved → 02ddf313…), KOHEN "Issuing a key" (the seed, once), secrets.yaml template. Tests: ledger_test (digest, entry, canonical, sign/verify, unsigned rendering, signed rendering); ledger_l3_test (own records listed newest first and only own; 404 alike for another's and none; an examination engraved under the key reads back signed and verifies against /peah/ledger_key). Elixir 642/0; L3 13/0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
e6c3702a22
commit
d6cf9355fd
13 files changed
+542
-13
No files matched your search
@@ -0,0 +1,109 @@
|
||||
defmodule VavPipeline.LedgerL3Test do
|
||||
@moduledoc "The ledger over L3: a key reads its own bedikot back, signed, and no one else's (2026-10-21)."
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
import Plug.Test
|
||||
import Plug.Conn
|
||||
|
||||
alias VavPipeline.BigdeiKodesh.Router
|
||||
alias VavPipeline.L3.Store
|
||||
|
||||
@opts Router.init([])
|
||||
@moduletag :l3
|
||||
|
||||
setup do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(VavPipeline.Repo)
|
||||
Ecto.Adapters.SQL.Sandbox.mode(VavPipeline.Repo, {:shared, self()})
|
||||
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(VavPipeline.Repo, :manual) end)
|
||||
write_provision()
|
||||
:ok
|
||||
end
|
||||
|
||||
defp write_provision do
|
||||
VavPipeline.MannaLedger.reset()
|
||||
on_exit(fn -> VavPipeline.MannaLedger.reset() end)
|
||||
today = Date.utc_today() |> Date.to_iso8601()
|
||||
expires = DateTime.utc_now() |> DateTime.add(6 * 3600) |> DateTime.to_iso8601()
|
||||
provision = %{"date" => today, "provision" => %{"compute_units" => 50, "memory_mb" => 512, "api_calls_external" => 10, "storage_delta_mb" => 2, "sanhedrin_panels_max" => 2},
|
||||
"type" => "regular", "expires_at" => expires, "written_by" => "ledger_l3_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))
|
||||
on_exit(fn -> File.rm(path) end)
|
||||
end
|
||||
|
||||
defp attrs(ref, key_id) do
|
||||
%{
|
||||
bedikah_ref: ref, provenance: "claude", query: "q", output_sha256: String.duplicate("cd", 32), output_length: 5,
|
||||
vital_content: "", verdict: "empty", examined_at: DateTime.utc_now() |> DateTime.truncate(:second), key_id: key_id
|
||||
}
|
||||
end
|
||||
|
||||
defp get(path, token) do
|
||||
conn(:get, path) |> put_req_header("authorization", "Bearer #{token}") |> Router.call(@opts)
|
||||
end
|
||||
|
||||
# An examination the offering protocol finds nothing in: the :empty branch, no engine needed.
|
||||
defp empty_examination do
|
||||
%{"provenance" => "claude", "query" => "status?", "vital_content" => "", "propositions" => [], "discarded" => ["Rolled out."],
|
||||
"references" => [], "hebrew" => [], "roots" => [], "authority_claims" => [], "actions" => [],
|
||||
"self_validation" => [], "self_promotion" => [], "excluded_adversarial" => [], "frames" => [], "sources" => [], "claims" => []}
|
||||
end
|
||||
|
||||
test "the record keeps the key's digest, and the ledger lists only that key's records, newest first" do
|
||||
mine = VavPipeline.Ledger.key_id("test-token")
|
||||
other = VavPipeline.Ledger.key_id("someone-else")
|
||||
{:ok, _} = Store.record_bedikah(attrs("bedikah-l3aaaa0000000001", mine))
|
||||
{:ok, _} = Store.record_bedikah(attrs("bedikah-l3aaaa0000000002", mine))
|
||||
{:ok, _} = Store.record_bedikah(attrs("bedikah-l3bbbb0000000003", other))
|
||||
{:ok, _} = Store.record_bedikah(attrs("bedikah-l3cccc0000000004", nil))
|
||||
|
||||
refs = Store.list_bedikot(mine) |> Enum.map(& &1.bedikah_ref)
|
||||
assert refs == ["bedikah-l3aaaa0000000002", "bedikah-l3aaaa0000000001"]
|
||||
|
||||
conn = get("/courtyard/ledger?limit=10", "test-token")
|
||||
assert conn.status == 200
|
||||
body = Jason.decode!(conn.resp_body)
|
||||
assert body["key_id"] == mine
|
||||
assert body["count"] == 2
|
||||
assert Enum.map(body["entries"], & &1["entry"]["bedikah_ref"]) == ["bedikah-l3aaaa0000000002", "bedikah-l3aaaa0000000001"]
|
||||
# never the text, never the key, never the owner column
|
||||
e = hd(body["entries"])["entry"]
|
||||
refute Map.has_key?(e, "key_id")
|
||||
assert e["output_sha256"] == String.duplicate("cd", 32)
|
||||
assert hd(body["entries"])["signed"] == false
|
||||
end
|
||||
|
||||
test "one record by ref: mine answers, another key's and a missing one answer 404 alike" do
|
||||
mine = VavPipeline.Ledger.key_id("test-token")
|
||||
{:ok, _} = Store.record_bedikah(attrs("bedikah-l3dddd0000000005", mine))
|
||||
{:ok, _} = Store.record_bedikah(attrs("bedikah-l3eeee0000000006", VavPipeline.Ledger.key_id("someone-else")))
|
||||
|
||||
assert get("/courtyard/bedikah/bedikah-l3dddd0000000005", "test-token").status == 200
|
||||
assert get("/courtyard/bedikah/bedikah-l3eeee0000000006", "test-token").status == 404
|
||||
assert get("/courtyard/bedikah/bedikah-l3ffff0000000007", "test-token").status == 404
|
||||
end
|
||||
|
||||
test "an examination is engraved under the caller's key and reads back signed and verifiable" do
|
||||
Application.put_env(:vav_pipeline, :ledger_signing_key, Base.encode64(:crypto.strong_rand_bytes(32)))
|
||||
mine = VavPipeline.Ledger.key_id("test-token")
|
||||
|
||||
try do
|
||||
examiner = fn _o, _p, _q, _s -> {:ok, empty_examination()} end
|
||||
assert {:ok, r} = VavPipeline.Bedikah.examine("Rolled out.", "claude", "status?", examiner: examiner, key_id: mine)
|
||||
assert r.verdict == :empty
|
||||
assert Store.get_bedikah(r.bedikah_ref).key_id == mine
|
||||
|
||||
back = get("/courtyard/bedikah/#{r.bedikah_ref}", "test-token")
|
||||
assert back.status == 200
|
||||
body = Jason.decode!(back.resp_body)
|
||||
assert body["signed"] == true
|
||||
pub = Jason.decode!(get("/peah/ledger_key", "x").resp_body)["public_key"]
|
||||
canonical = VavPipeline.Ledger.canonical(body["entry"])
|
||||
assert :crypto.hash(:sha256, canonical) |> Base.encode16(case: :lower) == body["canonical_sha256"]
|
||||
assert VavPipeline.Ledger.verify(canonical, body["signature"], pub)
|
||||
after
|
||||
Application.delete_env(:vav_pipeline, :ledger_signing_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,97 @@
|
||||
defmodule VavPipeline.LedgerTest do
|
||||
@moduledoc "The provenance ledger: canonical form, signature, key digest (2026-10-21)."
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias VavPipeline.Ledger
|
||||
|
||||
@seed :crypto.strong_rand_bytes(32)
|
||||
|
||||
defp record do
|
||||
%VavPipeline.L3.BedikahRecord{
|
||||
id: 7,
|
||||
bedikah_ref: "bedikah-0123456789abcdef",
|
||||
provenance: "claude",
|
||||
query: "q",
|
||||
output_sha256: String.duplicate("ab", 32),
|
||||
output_length: 12,
|
||||
vital_content: "",
|
||||
propositions: [],
|
||||
references: ["Devarim 6:4"],
|
||||
authority_claims: [],
|
||||
actions: [],
|
||||
sources: [%{"id" => "system", "sha256" => "00", "length" => 2}],
|
||||
claims: [%{"index" => 1, "sentence" => "x", "anchors" => []}],
|
||||
structure: %{"self_validation" => []},
|
||||
frames: [],
|
||||
rebukes: nil,
|
||||
mesorah: [],
|
||||
coupling_trace: [],
|
||||
violations: [],
|
||||
verdict: "tov",
|
||||
examined_at: ~U[2026-10-21 12:00:00Z],
|
||||
key_id: "deadbeef",
|
||||
inserted_at: ~U[2026-10-21 12:00:01Z]
|
||||
}
|
||||
end
|
||||
|
||||
test "key_id is a digest of the key, never the key, and nil for none" do
|
||||
assert Ledger.key_id(nil) == nil
|
||||
id = Ledger.key_id("courtyard-secret-key")
|
||||
assert String.length(id) == 32
|
||||
refute id =~ "courtyard"
|
||||
assert id == Ledger.key_id("courtyard-secret-key")
|
||||
refute id == Ledger.key_id("another")
|
||||
end
|
||||
|
||||
test "the entry carries the examination's fields and neither the database's nor the owner's" do
|
||||
e = Ledger.entry(record())
|
||||
assert Map.keys(e) |> Enum.sort() == Enum.sort(Ledger.ledger_fields())
|
||||
refute Map.has_key?(e, :id)
|
||||
refute Map.has_key?(e, :key_id)
|
||||
refute Map.has_key?(e, :inserted_at)
|
||||
assert e.examined_at == "2026-10-21T12:00:00Z"
|
||||
end
|
||||
|
||||
test "canonical form sorts keys at every level and carries no whitespace" do
|
||||
a = Ledger.canonical(%{"b" => 1, "a" => %{"z" => [%{"y" => 2, "x" => 1}], "m" => "s"}})
|
||||
assert a == ~s({"a":{"m":"s","z":[{"x":1,"y":2}]},"b":1})
|
||||
# the same map with keys given in another order canonicalizes the same
|
||||
assert a == Ledger.canonical(%{"a" => %{"m" => "s", "z" => [%{"x" => 1, "y" => 2}]}, "b" => 1})
|
||||
# atom and string keys meet in the same place
|
||||
assert Ledger.canonical(%{b: 1, a: 2}) == ~s({"a":2,"b":1})
|
||||
end
|
||||
|
||||
test "a signature verifies with the public key and fails on one changed byte" do
|
||||
bytes = Ledger.canonical(Ledger.entry(record()))
|
||||
sig = Ledger.sign(bytes, @seed)
|
||||
pub = Ledger.public_key(@seed)
|
||||
assert Ledger.verify(bytes, sig, pub)
|
||||
refute Ledger.verify(bytes <> " ", sig, pub)
|
||||
refute Ledger.verify(bytes, sig, Ledger.public_key(:crypto.strong_rand_bytes(32)))
|
||||
refute Ledger.verify(bytes, "not base64!", pub)
|
||||
end
|
||||
|
||||
test "without a seed nothing is signed, and the rendering says so" do
|
||||
assert Ledger.sign("x", nil) == nil
|
||||
assert Ledger.public_key(nil) == nil
|
||||
r = Ledger.render(record())
|
||||
assert r.entry.bedikah_ref == "bedikah-0123456789abcdef"
|
||||
assert String.length(r.canonical_sha256) == 64
|
||||
# the test env has no signing key configured
|
||||
assert r.signed == false and r.signature == nil
|
||||
end
|
||||
|
||||
test "a rendering with a seed configured is signed and self-consistent" do
|
||||
Application.put_env(:vav_pipeline, :ledger_signing_key, Base.encode64(@seed))
|
||||
|
||||
try do
|
||||
r = Ledger.render(record())
|
||||
assert r.signed
|
||||
bytes = Ledger.canonical(r.entry)
|
||||
assert :crypto.hash(:sha256, bytes) |> Base.encode16(case: :lower) == r.canonical_sha256
|
||||
assert Ledger.verify(bytes, r.signature, Ledger.public_key())
|
||||
after
|
||||
Application.delete_env(:vav_pipeline, :ledger_signing_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user