Getting started
Ledvar represents the state of a system as a canonical, content-addressed tree, so two snapshots can
be compared and the drift between them named exactly. This page walks every command of the ledvar
CLI, start to finish — follow along and you will know how to use it.
The unit is a snapshot: a list of nodes, where a node is an identity (path) plus
content (a map of string sets). Everything below is a real example you can run.
Install
cargo install ledvar
Prefer a prebuilt binary? Grab one from the releases page (Linux, Windows, macOS) — and verify its signature before you trust it.
A real snapshot
Download one — a MySQL database captured as a snapshot. No need to type anything:
curl -O https://raw.githubusercontent.com/ledvar/ledvar/main/examples/snapshot-a.json
It looks like this — a header, then a tree of nodes:
{
"protocol_version": "0.1.0",
"origin_id": "db.example.internal",
"provider_name": "mysql",
"timestamp": 1718800000,
"tree": [
{ "path": ["mydb", "user:app"], "content": { "grants": ["SELECT", "INSERT"], "risk": ["Medium"] } },
{ "path": ["mydb", "user:readonly"], "content": { "grants": ["SELECT"], "risk": ["Low"] } },
{ "path": ["mydb", "config:max_connections"], "content": { "value": ["200"] } }
]
}
A node’s path is an array of segments (its address in the tree), and content maps each
attribute to a set of strings. That is the whole data model.
validate — is it well-formed?
Before anything else, check the structure:
$ ledvar validate snapshot-a.json
ok: well-formed (3 nodes)
validate enforces the rules of the spec — non-empty paths, string-only values, a proper version,
no duplicate keys. It is the gate: ill-formed input is refused, never hashed (you will see that
below).
hash — the fingerprints
Every node gets two hashes:
$ ledvar hash snapshot-a.json
[
{
"path": ["mydb", "user:app"],
"identity_key": "…",
"content_hash": "…"
},
…
]
identity_key— SHA-256 of the canonicalpath. It answers “which thing is this?” Two nodes with the same identity_key are the same node.content_hash— SHA-256 of the canonicalcontent. It answers “what does it hold right now?” Same identity, different content_hash → that node changed.
That pair is the entire basis of the diff: comparing two snapshots is just matching identity_keys and checking whether the content_hash moved.
canon — the exact bytes that get hashed
This is the command that explains why Ledvar is reproducible. Take a single node — a Bitcoin transaction — and show its canonical form:
curl -O https://raw.githubusercontent.com/ledvar/ledvar/main/examples/node-btc.json
$ ledvar canon node-btc.json
path ["tx","3a1f9c0d7e2b48a6f5c1029384756abdfe0011223344556677889900aabbccdd"]
content {"amount_btc":["0.5"],"block_height":["840000"],"fee_btc":["0.00012"],"inputs":["bc1qsender0exampleaddr"],"outputs":["bc1qchange0exampleaddr","bc1qrecipient0exampleaddr"]}
identity 49a22f9cc0d6781b8847bfb9700109cd9ef279b77ae9621e5774e45508300924
content# 153bce97fc1489cb45601409acf891044615471eb9b33aa2397265594bc3280b
Look closely: in the source file the outputs were listed as recipient, change, but here they come
out change, recipient — sorted. Keys are sorted too. That is the point: the order you write
things in does not affect the hash. Two systems that hold the same state, formatted differently,
produce byte-identical canonical bytes and therefore the identical hash. Without that, a drift check
could never agree with itself.
diff — what changed
Now the reason it all exists. Download a second snapshot of that same database, taken later:
curl -O https://raw.githubusercontent.com/ledvar/ledvar/main/examples/snapshot-b.json
$ ledvar diff snapshot-a.json snapshot-b.json
~ mydb / user:app
+ grants DROP
~ risk : Medium → High
+ mydb / user:backup
+ grants = LOCK TABLES, SELECT
+ risk = Low
- mydb / user:readonly
- grants = SELECT
- risk = Low
1 added, 1 removed, 1 modified, 1 unchanged
Read it like a git diff — + added, - removed, ~ modified. In plain terms: user:app gained a
DROP grant and its risk jumped Medium → High; a new user:backup appeared; user:readonly
is gone. That is drift you would want to catch — and it fell out of the hashes alone, no rules about
what a “user” or a “grant” is.
Want machine-readable output? Add -o json and each node comes back tagged Added / Removed /
Modified / Unchanged.
When the input is bad — it refuses
A drift tool that quietly swallows garbage cannot be trusted, so Ledvar doesn’t:
$ curl -sO https://raw.githubusercontent.com/ledvar/ledvar/main/examples/reject-bad-version.json
$ ledvar validate reject-bad-version.json
error: not well-formed: protocol_version is not MAJOR.MINOR.PATCH: "1.2.0-rc1"
$ echo $?
1
Ill-formed input gets a clear reason and a non-zero exit — never a hash. The whole
examples/ folder ships reject-* files, one
per rule, each demonstrating exactly what is turned away and why.
schema — the reference shape
Need the schema to validate against, or a Protobuf definition to build a collector?
ledvar schema --out json # a JSON Schema for a snapshot
ledvar schema --out proto # the Protocol Buffers definition
Both are non-normative conveniences — the normative source is always
SPEC.md.
It isn’t only infrastructure
Nothing above knew what a database is. Ledvar is domain-blind: anything that becomes a tree of
path + content nodes can be hashed and diffed the same way. You already ran it on a Bitcoin
transaction — and a wallet is just the natural extension of that.
A Bitcoin wallet as Ledvar. Model each transaction as a node: the path is its txid, and the
content carries the amount, fee, block height, inputs and outputs — exactly the node-btc.json you
hashed above. Snapshot the wallet’s transactions today, snapshot them tomorrow, and ledvar diff
tells you precisely what moved: a new transaction is an added node, and a pending one that
confirms shows up as a modified node (its block_height goes from unset to a real height). The
hashes are the audit trail — the wallet’s history, content-addressed, where any tampering changes a
digest and surfaces as drift.
The same shape fits a DNS zone, a retail product, a VM, a filesystem entry — the conformance vectors span seven unrelated domains on purpose. The tool never changes; only what you feed it does.
Where to go next
- Write a collector — produce snapshots from your own real state.
- Specification — the normative rules behind everything here.
- Verifying releases — check the signature on a binary you downloaded.