Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 canonical path. It answers “which thing is this?” Two nodes with the same identity_key are the same node.
  • content_hash — SHA-256 of the canonical content. 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