simple-ots
July 27, 2026 · View on GitHub
Hash your files. Anchor to Bitcoin. Prove any single file later, without revealing the rest.
simple-ots builds a SHA-256 Merkle tree over a set of files and stamps the root to Bitcoin via OpenTimestamps. For each file it generates one leaf per (path variant, DID) combination, so you can later prove a specific file to a third party with a short Merkle proof that exposes nothing else in the tree.
No ZKP. No external indexing. Just SHA-256 and a Merkle tree.
Install
# Requires Go 1.22+
go install github.com/didvc/simple-ots@latest
Stamping requires the OpenTimestamps client:
pip install opentimestamps-client
To build from source:
git clone https://github.com/didvc/simple-ots
cd simple-ots
./install.sh --local
Quick start
# Hash all Git-tracked files and stamp to Bitcoin
git ls-files | simple-ots
# Walk the current directory (no pipe needed)
simple-ots
# Offline (skip OTS)
git ls-files | simple-ots --no-ots
Output lands in .simple-ots/results/YYYYMMDD_HHMMSS/:
| File | Contents |
|---|---|
manifest.jsonl | One JSON line per leaf (file × path-variant × DID) |
root.hash | Merkle root, hex SHA-256 |
root.hash.ots | OpenTimestamps receipt |
Selective disclosure
Each file gets multiple leaf hashes, one per (path_variant, DID) pair. To prove a file later, hand the recipient that leaf's JSON plus its Merkle path (roughly 16 hashes). They verify up to the root, then run ots verify. Everything else in the tree stays invisible.
Put a config in .simple-ots/config.toml:
dids = ["did:web:example.com"]
path_variants = ["null", "filename", "relative"]
include_no_did = true
Leaves per file = len(path_variants) × (len(dids) + 1). The example above gives 6 per file.
See Configuration and Config Examples for more.
Verify
After a Bitcoin block confirms (~2 hours):
ots upgrade .simple-ots/results/TIMESTAMP/root.hash.ots
ots verify .simple-ots/results/TIMESTAMP/root.hash.ots
Flags
| Flag | Default | Description |
|---|---|---|
--no-ots | false | Skip OpenTimestamps stamping |
--config PATH | <out-dir>/config.toml | Config file, must exist if given |
--out DIR | ./.simple-ots | Where results are written |
--verbose | false | Print each file as it's hashed |
--version | Print version and exit | |
-h, --help | Show usage |
Environment variables
| Variable | Description |
|---|---|
SIMPLE_OTS_CONFIG_PATH | Config file path, overridden by --config |
SIMPLE_OTS_OUT_DIR | Output directory, overridden by --out |
OTS_CMD | Full ots invocation to use instead of the one on PATH |
export OTS_CMD="env PYENV_VERSION=3.9.9 ots" # useful with pyenv
export SIMPLE_OTS_CONFIG_PATH=~/configs/simple-ots.toml
$env:SIMPLE_OTS_CONFIG_PATH = "C:\pg2\.simple-ots\config.toml"
Each run prints the config file it loaded as its Config: line. A path given through --config or SIMPLE_OTS_CONFIG_PATH has to exist; a missing one stops the run instead of falling back to the defaults.
To keep results out of the repository root, set --out ./.personal/.simple-ots or put output_dir = "./.personal/.simple-ots" in the config file. A relative output_dir follows the working directory, so a single shared config file drops each project's results in that project's own folder.
How it works
Paths are sorted alphabetically. For each file: compute content_sha256 (raw bytes) and read mtime. For each (path_variant, DID) combo, build a JSON object with four keys (content_sha256, datetime, did, path), serialize with sorted keys, and SHA-256 the result. All leaf hashes go into an in-memory Merkle tree. The root is 32 bytes regardless of how many files are in the tree.
Man page
man simple-ots
The man page is installed automatically by install.sh --local if a writable man directory is found.
Contributing
See CONTRIBUTING.md.
Citation
Use the "Cite this repository" button on the GitHub repo, or see CITATION.cff.
License
Related projects
- c2pa — C2PA examined from the standpoint of artistic authorship