Skip to main content

btcvm

A minimal register machine that executes in lockstep with Bitcoin blocks.

Each Bitcoin block triggers a fixed number of VM cycles. After each block, the VM's state is hashed and cryptographically bound to the block hash — producing a verifiable ledger of computation anchored to Bitcoin's clock.

Why

Bitcoin is a clock. Every ~10 minutes a new block is mined, and its hash is unpredictable until the moment it's found. This makes it a natural source of consensus timing for off-chain computation.

btcvm demonstrates the minimal architecture for a Bitcoin-clocked state machine:

Bitcoin block hash
      ↓
  VM executes N cycles
      ↓
  commitment = SHA256(block_hash + state_hash)
      ↓
  OP_RETURN → committed to Bitcoin (optional)

The resulting ledger is independently verifiable: anyone with the block hashes (publicly available from any Bitcoin node or API) can recompute every commitment and confirm the VM ran exactly as recorded.

Files

File Description
vm.py Minimal register machine (8 registers, 7 opcodes)
clock.py Bitcoin block clock via blockstream.info API
programs.py Sample programs (fibonacci, countdown)
vdf.py VDF sub-clock — sequential SHA256 chain with tick/verify (v1.2)
trace.py Step-by-step execution trace with Merkle root commitment (v2)
fleet.py N parallel VMs with fleet Merkle root — one OP_RETURN anchors all (v3)
broadcast.py Optional OP_RETURN broadcast via the bit library
main.py Orchestrator — runs clock loop, executes VM(s), writes ledger
verify.py Verifies ledger, VDF chain, trace, and fleet Merkle against Bitcoin
ott.py Bitcoin-anchored media archive — images, video, and git repos; also an interactive shell
test_vm.py Unit tests (VM, VDF, trace, fleet)
test_ott_completion.py Unit tests (ott shell tab completion, path handling)
Makefile Common tasks: install, test, lint, run, ott
completions/ Bash and Zsh tab completions for btcvm and ott

VM

The VM has 8 registers (R0R7) and 7 opcodes:

Opcode Description
LOAD r, val Load immediate value into register
ADD dst, a, b Rdst = Ra + Rb
SUB dst, a, b Rdst = Ra - Rb
MUL dst, a, b Rdst = Ra * Rb
JMP addr Unconditional jump
JZ r, addr Jump if register is zero
HALT Stop execution

Programs are tuples loaded into memory. See programs.py for examples.

Usage

Requirements: Python 3.11+, no runtime dependencies (stdlib only).

# Install from PyPI — https://pypi.org/project/btcvm/
pip install btcvm

# ...or from a local checkout, for development
pip install -e .        # editable install
make install            # same via Makefile
make dev                # with dev deps (pytest, ruff)

Installs as flat top-level modules (import ott, not import btcvm.ott) and exposes two commands: btcvm (the register machine) and ott (the media archive — see below).

btcvm clock

# v1 — one entry per Bitcoin block
python3 main.py fibonacci

# v1.2 — VDF sub-clock: 5 ticks per block
python3 main.py fibonacci --vdf-ticks 5

# v2 — trace Merkle root replaces state_hash
python3 main.py fibonacci --trace

# v3 — fleet of 4 parallel VMs, single fleet root per OP_RETURN
python3 main.py fibonacci --vms 4

# Everything combined
python3 main.py fibonacci --vms 4 --vdf-ticks 3 --trace

# Optional: broadcast commitment as OP_RETURN
pip install bit
python3 main.py fibonacci --broadcast --wif <your-WIF-key> --network mainnet

Getting a wallet for broadcast:

from bit import PrivateKey
k = PrivateKey()
print(k.to_wif(), k.address)

Fund the address with ~5000 sats (covers ~8 commitments at low fee rates).

Verifying a ledger:

python3 verify.py                          # block hashes + commitments
python3 verify.py --trace-file trace.jsonl # include trace verification
python3 verify.py --check-txs              # also fetch block tx lists

ott — Bitcoin-anchored media archive

ott stores images and video in a content-addressed Merkle tree and commits the root to Bitcoin via the btcvm ledger. Images are hashed whole; video files are split into 256 KB chunks with a per-file Merkle tree, enabling byte-range inclusion proofs.

image.jpg  → SHA256 ──────────────────────────────────┐
                                                       ├─→ global root → Bitcoin
video.mp4  → [chunk₀, chunk₁, …, chunkₙ] → file root ┘
# Stage files (images or video — detected by extension). Hashed now — that's
# what identifies the file and catches duplicates — but not yet copied into
# object storage or written to the manifest.
python3 ott.py add photo.jpg family.jpg video.mp4

# Show what's staged vs. already archived, and the current Merkle root
python3 ott.py status

# Changed your mind about a staged file? Drop it before it's archived.
# (Only works pre-commit — once a file's archived, rm can't touch it.)
python3 ott.py rm family.jpg

# Archive everything staged, then commit the Merkle root to the btcvm
# ledger (sync is a plain alias for commit — same thing, either name)
python3 ott.py commit

# List all archived files
python3 ott.py list

# Generate a wallet key, then anchor the commitment on-chain (optional)
python3 ott.py keygen --network testnet
python3 ott.py broadcast --wif <WIF_KEY>

# Verify a file is in the archive (Merkle inclusion proof)
python3 ott.py verify photo.jpg

# Prove a specific 256 KB chunk of a video is in the archive
python3 ott.py verify-chunk video.mp4 3

# Verify every ledger commit against real Bitcoin, not just internal state
python3 ott.py verify-chain

Interactive shell:

python3 ott.py            # or: python3 ott.py shell

The shell wraps the same archive with a stateful cd-able hierarchy, tab completion on every command (archive names, tags, local paths), and a few things the flat CLI doesn't have:

Command Description
add [-r] <file>... Stage files (git-index style) — hashed now, archived on commit/sync
rm <name_or_hash> Unstage a pending add; refuses once a file's actually committed
commit / sync Archive everything staged, then commit the Merkle root to the ledger (same action, either name)
l [-a] [-t tag] [-b] [dir] One-level ls-style view of the archive hierarchy (from orig_path)
tree [-a] [-t tag] [-dN] [-b] [dir] Recursive tree view; -d0 for unlimited depth
cd, pwd Navigate the archive hierarchy (real filesystem nav is lcd/lpwd) — cd'ing to a repo's name instead jumps the real filesystem cwd to that repo's checkout, since repos have no virtual file tree of their own
list / ls [pattern] Full flat dump of every path in one table, optionally filtered by regex (bare or /slash-delimited/)
open <name_or_hash> (alias o) Open an archived file with the OS default handler (prefers the live copy, falls back to the archived one)
mv <name> <new_path> Update an entry's tracked path; moves into an existing dir like real mv
reindex [root] One indexed filesystem scan — relocates every stale entry and re-anchors orig_path to the archive root
tag <add|rm|list> <pattern> <tagname> Bulk-tag entries by regex match against their archive path
repo <add|list|verify|update|outdated|update-all|tag|verify-tag|qr> Track a git repo's HEAD and (optionally) a GPG-signed release tag in the archive; outdated (alias o) checks every tracked repo against its remote, update-all (alias ua) pulls all of them
backfill [--workers N] Store archive copies for entries on disk but not yet uploaded to the active backend — also the migration path when switching backends (e.g. local → S3). N concurrent (default 8, or OTT_BACKFILL_WORKERS); live progress bar on a TTY, one line per file when piped
verify-objects [--workers N] Audit every archived entry against the active backend (existence + size) — for S3 this always queries the bucket directly, bypassing the local cache, so it's the real answer to "did this actually upload"
bump-fee --wif <WIF> [--fee SAT_PER_VBYTE] [--network testnet|mainnet] CPFP fee bump for a stuck broadcast tx — spends the pending change output at a higher fee so miners confirm both together (not RBF). Default 10 sat/vbyte
verify-chain [-c] Verify every ledger commit against real Bitcoin (refetches each block's actual hash) — -c also checks the OP_RETURN landed
keygen [--network] Generate a wallet key for broadcast (prints WIF + a QR of the address only, never the key)
broadcast [--wif] Broadcast a commitment as a Bitcoin OP_RETURN tx
qr [hash|file] QR code for a hash, a file's SHA256, or the current Merkle root
!<cmd> Run a real shell command without leaving the shell

Entries missing at their last known path are hidden by default in l/tree (-a shows them); ott reindex is the real fix.

add only stages — nothing's copied into object storage or written to the manifest until commit/sync. Names/hashes resolve relative to the current archive directory first (cd-aware), only falling back to a global search if nothing matches locally.

Via Makefile:

make add FILE=photo.jpg
make verify-file FILE=photo.jpg
make ott-status
make ott-list
make ott-commit
make ott-clean      # remove manifest + ledger

Environment variables:

Variable Default Description
OTT_MANIFEST ott_manifest.jsonl Manifest path
OTT_LEDGER ott_ledger.jsonl Ledger path
OTT_CHUNK_BYTES 262144 (256 KB) Video chunk size
OTT_HOME ~/.ott Archive root when not using a local .ott/ dir
OTT_BACKEND local Storage backend — local or s3 (needs pip install btcvm[s3])
OTT_S3_BUCKET Required when OTT_BACKEND=s3
OTT_S3_PREFIX '' Optional key prefix within the bucket
OTT_S3_CACHE_DIR .ott/cache Local cache dir for S3-backed objects
OTT_BACKFILL_WORKERS 8 Default concurrency for backfill/verify-objects

Proof chain for a video chunk:

chunk N bytes → SHA256 → chunk hash
                             ↓ Merkle proof (steps: log₂ chunks)
                         file root  (= global leaf)
                             ↓ Merkle proof (steps: log₂ files)
                         global root → committed to Bitcoin

verify-chunk outputs all three levels: bytes-on-disk match, per-file proof, global proof.

Architecture

VDF sub-clock (v1.2)

Between Bitcoin blocks the VM fires once per VDF tick. Each tick is STEPS_PER_TICK sequential SHA256 evaluations seeded from the previous tick's output. Because SHA256 can't be parallelised in this chained form, the ticks represent a minimum sequential cost any verifier must replay.

Bitcoin block hash B_h
  tick 0: SHA256ᴺ(B_h)    → vdf₀ → VM cycles → commit₀
  tick 1: SHA256ᴺ(vdf₀)   → vdf₁ → VM cycles → commit₁
  …
  Next block B_{h+1} reseeds the chain

Trace commitment (v2)

Every VM step is recorded as a hash-chained entry: step_hash = SHA256(prev_hash ‖ pc ‖ op ‖ regs_before ‖ regs_after). A binary Merkle tree over all step hashes produces a single root that replaces state_hash in the ledger commitment. Any individual step can be verified without replaying the full execution. The trace file is the witness a ZK prover (RISC Zero, SP1, Cairo) would consume.

Fleet Merkle root (v3)

N VMs run in parallel each tick. A binary Merkle tree over all N commitments produces a single fleet_root — one OP_RETURN regardless of fleet size.

tick N:
  VM₀ → commitment₀ ─┐
  VM₁ → commitment₁ ─┤ Merkle → fleet_root → SHA256(block_hash:fleet_root) → OP_RETURN
  VM₂ → commitment₂ ─┤
  VM₃ → commitment₃ ─┘

Ledger formats

v1 entry:

{
  "block_height": 961224,
  "block_hash": "000000000000...",
  "vdf_tick": 0,
  "vm_ticks": 10,
  "halted": false,
  "registers": [1, 1, 1, 20, 1, 0, 0, 0],
  "state_hash": "a3f9c2b1...",
  "commitment": "88d6f091..."
}

v1.2 additions (VDF active):

{
  "vdf_tick": 2,
  "vdf_input": "prev_tick_output...",
  "vdf_hash": "this_tick_output...",
  "commitment": "SHA256(block_hash:vdf_hash:state_hash)"
}

v2 additions (trace active):

{
  "trace_root": "merkle_root_over_all_steps...",
  "trace_steps": 42,
  "commitment": "SHA256(block_hash:trace_root)"
}

v3 (fleet active) replaces per-VM fields with:

{
  "fleet_size": 4,
  "fleet_root": "merkle_root_over_vm_commitments...",
  "commitment": "SHA256(block_hash:fleet_root)",
  "vms": [
    {"vm_id": 0, "program": "fibonacci", "halted": false, "registers": [...], "state_hash": "..."},
    ...
  ]
}

ott ledger entry:

{
  "ts": "2026-08-17T20:37:00Z",
  "block_height": 961301,
  "block_hash": "000000000000...",
  "merkle_root": "b699603c...",
  "commitment": "9dc75674...",
  "image_count": 3
}

Tab completions

make completion

Installs Bash and Zsh completions for both btcvm and ott:

  • btcvm — flags and values (--vdf-ticks, --vms, etc.)
  • ott add — any file
  • ott verify — filenames from the manifest
  • ott verify-chunk — video names, then chunk indices from the manifest

For Zsh, add to ~/.zshrc if not already present:

fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit && compinit

Makefile targets

make install        pip install -e .
make dev            install with dev deps
make test           run pytest
make lint           ruff check
make fix            ruff --fix
make run            btcvm one block
make run-vdf        btcvm with VDF sub-clock
make run-trace      btcvm with trace Merkle
make run-fleet      btcvm fleet of 4 VMs
make verify         verify ledger.jsonl
make ott-status   show archive status
make ott-list     list archived files
make ott-commit   commit Merkle root
make ott-clean    remove manifest + ledger
make ott-repo-add                 update archived repo record to HEAD
make ott-tag [OTT_NEXT_TAG=v1.x]  sign a git tag + record fingerprint in ott
make ott-push [OTT_NEXT_TAG=v1.x] push commits + tag, then commit ott root
make ott-snapshot                 repo-add + commit root (no tag, no push)
make ott-release [OTT_NEXT_TAG=v1.x] full: tag → push → commit root
make add FILE=…     add file to ott
make verify-file FILE=…  verify inclusion
make completion     install shell completions

Roadmap

  • v1 — Bitcoin-clocked register machine, local ledger, optional OP_RETURN
  • v1.1 — Ledger verification against block hashes and OP_RETURN
  • v1.2 — VDF sub-clock (--vdf-ticks N)
  • v2 — Trace commitment / Merkle root (--trace)
  • v3 — Fleet Merkle: N parallel VMs, single OP_RETURN (--vms N)
  • ott — Bitcoin-anchored media archive with chunked video and inclusion proofs

Tests

make test
# or
python3 -m pytest -v

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

btcvm-1.3.8.tar.gz (72.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

btcvm-1.3.8-py3-none-any.whl (75.9 kB view details)

Uploaded Python 3

File details

Details for the file btcvm-1.3.8.tar.gz.

File metadata

  • Download URL: btcvm-1.3.8.tar.gz
  • Upload date:
  • Size: 72.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.16

File hashes

Hashes for btcvm-1.3.8.tar.gz
Algorithm Hash digest
SHA256 7adcc3bef1735d17fcf3ea6f5544f30a20c1dcd0a08e82e99d24b90cfbeca274
MD5 61dd0bfaa2f6f19254bfdc183e6890fc
BLAKE2b-256 bd3812bcaa317ebded525ef9ea8cb1eeff26caf79b3e58bd012c5a927e3f4846

See more details on using hashes here.

File details

Details for the file btcvm-1.3.8-py3-none-any.whl.

File metadata

  • Download URL: btcvm-1.3.8-py3-none-any.whl
  • Upload date:
  • Size: 75.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.16

File hashes

Hashes for btcvm-1.3.8-py3-none-any.whl
Algorithm Hash digest
SHA256 922cff237f44d015b0921686d3e33a1d36c79aba7ba94812e8dcf7c36ffe7e0a
MD5 6b7f4f30d43db8335d14cc63ac28b90f
BLAKE2b-256 bdf8444455cd976d879582cbb8655e1d51d2bcec3b697cd5f33a83908c72d647

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.3.8 This release

2 files

1.0.0

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page