Most things you could store this way involve a trade: you give up sorting, or joins, or full-text, in exchange for privacy. An audit trail is the case where there is no trade — the shape of the problem and the shape of this database are the same shape.
| An audit trail needs | What this already is |
|---|---|
| Append-only history | The index is per-writer append-only counter chains. Rewriting history was never expressible, so nothing has to forbid it. |
| Tamper-evidence | Every record is AES-GCM. An altered blob does not decrypt — a holder cannot quietly change an entry, only destroy one, and the replica group makes that visible. |
| Filter by actor, action, time | Prefix, prefix and range — the three operations this index is fastest at. Ordered by time for free, because dyadic leaves are already in value order. |
| A holder who can't read it | Nodes hold pseudorandom keys and opaque blobs. |
That last row is the whole argument. For most data, "the vendor can read it" is something you tolerate. For an audit trail it is a liability — which makes this the rare case where being unable to read is the feature rather than the compromise.
There is no plugin to install and none to write. Vector, Fluent Bit and the OpenTelemetry Collector all ship a generic HTTP output, so the entire integration is a few lines of config:
[sinks.blindrange] type = "http" inputs = ["your_source"] uri = "http://127.0.0.1:8710/ingest" method = "post" encoding.codec = "json"
[OUTPUT]
Name http
Match *
Host 127.0.0.1
Port 8710
URI /ingest
Format jsonexporters:
otlphttp/blindrange:
logs_endpoint: http://127.0.0.1:8710/ingest
encoding: json
service:
pipelines:
logs:
exporters: [otlphttp/blindrange]curl -X POST http://127.0.0.1:8710/ingest \
-H 'Content-Type: application/json' \
-d '{"actor":"alice@corp","action":"role.grant","target":"billing-admin"}'Ingest takes a single object, a JSON array, NDJSON, or an
OTLP/JSON envelope — and it unwraps OTLP into individual records
rather than storing a whole batch as one blob, which is the failure mode
that would leave a pipeline looking healthy while quietly destroying the
trail. Timestamps are read in seconds, milliseconds, nanoseconds or
RFC 3339, from any of five key names; the actor from actor,
user, principal or subject. Anything
unrecognised still rides along inside the encrypted payload. Being
forgiving here is deliberate: an audit pipeline that drops events over a
key name is worse than no pipeline.
Log agents are one way in. The other is that almost everything which archives data already writes to S3 — backup tools, compliance archivers, Loki, rclone. There is an S3-compatible gateway in front of the same storage, so those tools can target it today rather than being ported. Same encryption, same blindness, and prefix listings stay native because they are index lookups rather than scans.
# rclone, as one example — full configs and limits on the S3 page
rclone copy ~/documents blindrange:archive/documents
python3 examples/auditlog/audit.py \
--state ~/.blindrange/audit.brdb \
--bootstrap seed.blindrange.dev:7501 \
--secret blindrange-public \
--account <key from tokens.blindrange.dev/signup>
# → http://127.0.0.1:8710
The keys live in that process — and in the gateway's, if you run it —
and in the .brdb file beside them. Never on a node, never with
us. Run them next to whatever produces your events.
This is the same fork Storj draws between its hosted S3 gateway, where keys sit server-side, and the self-hosted one that keeps encryption end-to-end. Hosting it for you would be more convenient and would cost exactly the property you came for, so we don't offer it.
--leaf sets the time resolution that nobody can ever
exceed — not a node operator, not someone logging every query you run,
however long they watch. It is also what you pay: finer resolution means
more dyadic levels, so more index entries per event.
--leaf 3600 → leaf_width 4096 (1.1 hours) --leaf 60 → leaf_width 64 (1.1 minutes)
Widths must be powers of two, so a human interval is snapped — and the tool prints the bound you actually got, not the one you asked for. Calling 4096 seconds "an hour" would be a lie in the one number that is the guarantee. Coarser is cheaper and more private, which is the unusual direction; see the calculator.
An audit trail is only as good as your ability to prove nothing was removed, and that question applies to us as much as to a node operator. Two different mechanisms cover the two halves, and it is worth keeping them apart.
| Question | What answers it |
|---|---|
| Did a node lose my events? | You audit it: sample your own records and check the AES-GCM tag. A node cannot fabricate a hit — it cannot derive the key or forge the ciphertext. |
| Did the network quietly drop a node's failures, or restate what it was paid? | Every accepted audit and every share calculation is a leaf in an append-only Merkle log. Keep one tree head and any later rewrite of anything before it fails a consistency proof. |
# remembers the head; next run demands proof the log still contains it python3 examples/verify_log.py head size 3 root 4cd723911d1ce43e… signature valid consistent with the 2 entries seen previously ✓ (1 new)
The head is published at
status.blindrange.dev/log/sth,
with the entries themselves at /log/entries. Reports are logged
whole — they carry no owner or database identifier by design — so you
can re-run the scoring over the logged inputs and check the arithmetic, not
merely that nobody edited the answer afterwards. Tested by actually
cheating: rewriting a past share from 333 to 900, and separately deleting an
entry; an operator holding the earlier head rejects both.
What it does not do: it cannot stop us declining to log something in the first place, and it cannot by itself catch a split view — one log shown to you and a different one to someone else. That needs two operators comparing heads, which is why the verifier prints one in a form you can paste to somebody.