Reading an evidence record
A drill produces exactly one record, appended to the evidence log. It is the product — the test is the means — so it is worth being able to read one without consulting the schema.
{ "adapter": { "digest": "sha256:4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c", "name": "postgres", "protocol": "probavi-adapter/0", "version": "0.1.0" }, "backup": { "checksum": "sha256:9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f", "created_at": "2026-07-30T01:58:02.000Z", "kind": "pgdump", "size_bytes": 565248 }, "checks": [ { "detail": "accepting connections", "name": "service_healthy", "ok": true }, { "detail": "100000 rows (min 100000)", "name": "row_count:orders", "ok": true } ], "drill": { "config_hash": "sha256:7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d", "name": "prod-orders-db", "pitr_target": "2026-07-30T14:32:00.000Z" }, "env": { "arch": "amd64", "host_id": "3f7a9c2e5b1d8e04", "os": "linux", "probavi_digest": "sha256:1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d", "probavi_version": "0.1.0" }, "error": null, "outcome": "pass", "prev_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", "sandbox": { "params": { "image": "postgres:16", "memory": "2GiB" }, "provider": "docker" }, "schema": "probavi-evidence/2", "seq": 1, "sig": { "alg": "ed25519", "key_id": "56475aa75463474c", "sig_b64": "/f2ahJgj08qDVSvbTIZ/+XI7IfkCavp0WfS7ETIezC+E2PMzFzEmX/G8d4d+S42Q9ZegQDU8aMKJTBb/gS5KDQ==" }, "timings_ms": { "engine_ready": 1166, "provision": 1170, "restore": 190, "total": 2840, "transfer": 110, "validate": 61 }, "ts": "2026-07-31T02:00:11.482Z"}A real record from the format’s published example log, re-indented for reading — on disk it is one canonical line, and that is the form the signature covers.
Signed with:03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8
What each part says
Section titled “What each part says”Where it sits in the chain. seq is the position, prev_hash the SHA-256
of the record before it, and ts when the drill finished. The first record in
a log carries an all-zero prev_hash because it has no predecessor. Alter any
record and every link after it stops verifying, which is what makes the log
append-only in practice and not merely by intention.
What was drilled. drill names the target and carries a hash of the
configuration used, so two records can be compared for whether they were
produced by the same drill or by a drill someone edited in between. Where a
point-in-time restore was requested, the target time is recorded with it.
What it was drilled from. backup records the source kind, the archive’s
size, its checksum, and when it was created. The creation time is the field to
read first when a pass looks too good: a record that proves a three-month-old
archive restores is a true statement about a backup nobody should be relying
on.
Where it was restored. sandbox names the provider and the parameters it
was given — the image, the memory limit — recorded verbatim. adapter names
the adapter, its version, the protocol version it spoke, and digest, the
SHA-256 of the adapter executable the core launched. Version and digest are not
the same fact: the version is a number the adapter reports about itself, so two
different builds can share one, and the digest is what tells them apart.
Together these are what make a restore duration comparable with another one:
same image, same limits, same adapter build, or the two numbers are not
measuring the same thing.
What happened. outcome is the drill’s verdict, and error carries the
reason when there is one. checks lists every check by name with its result
and a short detail — the row count that was found and the bound it was measured
against, not merely that it passed.
How long it took. timings_ms breaks the drill into phases: provisioning
the sandbox, transferring the archive, the restore itself, waiting for the
engine, and validation. The restore phase is measured separately from the
checks on purpose, so the number you compare against a recovery time objective
is not inflated by however much SQL you chose to run afterwards.
Where it ran. env records the operating system, architecture, a stable
host identifier, the Probavi version and probavi_digest, the SHA-256 of the
probavi executable that wrote the record. Enough to tell two drill hosts
apart, to explain a change in timings, and to say which build produced the
proof; not enough to identify anything else, because nothing about the drill
leaves the machine. Either digest is null where the file could not be read — a
digest is worth recording, never worth failing a drill for.
The signature. sig carries the algorithm, the key identifier and the
signature itself, over the canonical single-line form of the record.
What verification proves
Section titled “What verification proves”Checking a log takes the file and a public key — no network, no licence, and not even a Probavi installation, since the format ships with an independently released verifier.
What it establishes is precise: that these records were produced by the
holder of that key and have not been altered since. Every signature is
valid, and every prev_hash matches the record it points at, so nothing has
been edited, reordered or removed from the middle — which is exact, and leaves
one removal for the section below.
The one removal the file cannot show
Section titled “The one removal the file cannot show”The exception is at the end. Delete the newest records from a log and what remains is a shorter log whose sequence still starts at 1 and whose chain is still unbroken, so a verifier reading only the file answers VALID — correctly, because nothing given that file alone can tell a truncated log from a log that is simply shorter. The next drill appends onto the shortened chain, so the removal leaves no trace afterwards either.
Closing it takes one value kept outside the file. Every verification prints its
head: the highest seq it reached, together with the SHA-256 of that
record’s stored line, written <seq>:sha256:<hex>. Keep the head one run
reports, and hand it to the next as --anchor.
A log that still carries that line unchanged has grown since, which is the
ordinary case, and the answer is the one the file would have given anyway. A log
that ends before that seq is missing records from its end. A log whose line at
that seq hashes differently was truncated and then grown again — long enough,
and wrong where it matters. The last two are INVALID and exit 2, because a log
shorter than its anchor is not the log that anchor was taken of, which is the
same finding as a record removed from within it.
The strength of an anchor is entirely in where it is kept, so keep it where the
log’s writer cannot reach — a ticket, a mail, a commit in a repository the drill
host cannot write to, or a probavi push receiver that retains what it was
sent. Probavi keeps none of them: it prints the value and stops. Signing the
head would add nothing, because the attacker this format is built against is the
one holding the signing key.
None of this moves the format. No field is added, nothing further is signed, and
records are still probavi-evidence/2 — an anchor is an input to verification,
and without one the file and the public key remain sufficient for everything the
chain alone can prove.
What it deliberately does not prove
Section titled “What it deliberately does not prove”Verification is a statement about the record, not about your recovery position. It cannot tell you that the backup was of the right database, that the checks were the right checks, or that the restore duration is inside your objective. A perfectly valid signature over a record of a drill with one healthcheck and no other assertion proves that the drill happened and reports what little it asserted.
The two digests are narrower than they look, and the specification says so rather than leaving it to be assumed. Each is the hash of a file read just before it was run, so it names the bytes Probavi selected — not proof that those bytes are the instructions that executed. A file replaced between the hash and the launch would go unrecorded. Closing that window means reading the running process’s own image, which does not exist on every platform Probavi supports, so the record makes the narrower claim it can keep.
That is not a limitation to work around. It is the division of labour: the signature makes the history trustworthy, and the checks you choose decide what the history is worth.