btcvm 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- asm.py +116 -0
- broadcast.py +84 -0
- btcvm-0.1.0.dist-info/METADATA +422 -0
- btcvm-0.1.0.dist-info/RECORD +18 -0
- btcvm-0.1.0.dist-info/WHEEL +5 -0
- btcvm-0.1.0.dist-info/entry_points.txt +3 -0
- btcvm-0.1.0.dist-info/licenses/LICENSE +21 -0
- btcvm-0.1.0.dist-info/top_level.txt +12 -0
- clock.py +44 -0
- fleet.py +121 -0
- imgfs.py +591 -0
- main.py +387 -0
- ott.py +3779 -0
- programs.py +3 -0
- trace.py +109 -0
- vdf.py +65 -0
- verify.py +245 -0
- vm.py +88 -0
asm.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Minimal two-pass assembler for the btcvm instruction set.
|
|
3
|
+
|
|
4
|
+
Syntax
|
|
5
|
+
------
|
|
6
|
+
; or # starts a line comment (inline too)
|
|
7
|
+
Labels end with a colon, either alone or prefixed to an instruction:
|
|
8
|
+
loop:
|
|
9
|
+
JZ R3, done
|
|
10
|
+
done: HALT
|
|
11
|
+
Registers: R0–R7 (case-insensitive)
|
|
12
|
+
Immediates: integer literals
|
|
13
|
+
|
|
14
|
+
Opcodes
|
|
15
|
+
-------
|
|
16
|
+
LOAD Rdst, imm
|
|
17
|
+
ADD Rdst, Ra, Rb
|
|
18
|
+
SUB Rdst, Ra, Rb
|
|
19
|
+
MUL Rdst, Ra, Rb
|
|
20
|
+
JMP label
|
|
21
|
+
JZ Rtest, label
|
|
22
|
+
HALT
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
import os
|
|
27
|
+
import glob
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_REG = re.compile(r'^R([0-7])$', re.IGNORECASE)
|
|
31
|
+
_SPLIT = re.compile(r'[\s,]+')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _reg(tok: str) -> int:
|
|
35
|
+
m = _REG.match(tok.strip())
|
|
36
|
+
if not m:
|
|
37
|
+
raise SyntaxError(f"expected register R0-R7, got {tok!r}")
|
|
38
|
+
return int(m.group(1))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _imm(tok: str) -> int:
|
|
42
|
+
try:
|
|
43
|
+
return int(tok.strip())
|
|
44
|
+
except ValueError:
|
|
45
|
+
raise SyntaxError(f"expected integer immediate, got {tok!r}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def assemble(source: str) -> list[tuple]:
|
|
49
|
+
"""Assemble source text into a list of VM instruction tuples."""
|
|
50
|
+
|
|
51
|
+
# Pre-process: strip comments, split into (label | None, instruction | None) pairs
|
|
52
|
+
raw_lines: list[tuple[str | None, str | None]] = []
|
|
53
|
+
for line in source.splitlines():
|
|
54
|
+
line = re.sub(r'[;#].*', '', line).strip()
|
|
55
|
+
if not line:
|
|
56
|
+
continue
|
|
57
|
+
if ':' in line:
|
|
58
|
+
label_part, _, rest = line.partition(':')
|
|
59
|
+
raw_lines.append((label_part.strip() or None, rest.strip() or None))
|
|
60
|
+
else:
|
|
61
|
+
raw_lines.append((None, line))
|
|
62
|
+
|
|
63
|
+
# Pass 1: assign a PC to each label
|
|
64
|
+
labels: dict[str, int] = {}
|
|
65
|
+
pc = 0
|
|
66
|
+
for label, instr in raw_lines:
|
|
67
|
+
if label is not None:
|
|
68
|
+
labels[label] = pc
|
|
69
|
+
if instr:
|
|
70
|
+
pc += 1
|
|
71
|
+
|
|
72
|
+
# Pass 2: emit instruction tuples
|
|
73
|
+
program: list[tuple] = []
|
|
74
|
+
for label, instr in raw_lines:
|
|
75
|
+
if not instr:
|
|
76
|
+
continue
|
|
77
|
+
parts = _SPLIT.split(instr.strip())
|
|
78
|
+
op = parts[0].upper()
|
|
79
|
+
args = parts[1:]
|
|
80
|
+
|
|
81
|
+
if op == 'LOAD':
|
|
82
|
+
program.append(('LOAD', _reg(args[0]), _imm(args[1])))
|
|
83
|
+
elif op in ('ADD', 'SUB', 'MUL'):
|
|
84
|
+
program.append((op, _reg(args[0]), _reg(args[1]), _reg(args[2])))
|
|
85
|
+
elif op == 'JMP':
|
|
86
|
+
target = args[0].strip()
|
|
87
|
+
if target not in labels:
|
|
88
|
+
raise SyntaxError(f"undefined label {target!r}")
|
|
89
|
+
program.append(('JMP', labels[target]))
|
|
90
|
+
elif op == 'JZ':
|
|
91
|
+
target = args[1].strip()
|
|
92
|
+
if target not in labels:
|
|
93
|
+
raise SyntaxError(f"undefined label {target!r}")
|
|
94
|
+
program.append(('JZ', _reg(args[0]), labels[target]))
|
|
95
|
+
elif op == 'HALT':
|
|
96
|
+
program.append(('HALT',))
|
|
97
|
+
else:
|
|
98
|
+
raise SyntaxError(f"unknown opcode {op!r}")
|
|
99
|
+
|
|
100
|
+
return program
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def assemble_file(path: str) -> list[tuple]:
|
|
104
|
+
with open(path) as f:
|
|
105
|
+
return assemble(f.read())
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def discover(directory: str | None = None) -> dict[str, list[tuple]]:
|
|
109
|
+
"""Load and assemble every .asm file in *directory* (default: programs/)."""
|
|
110
|
+
if directory is None:
|
|
111
|
+
directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'programs')
|
|
112
|
+
result: dict[str, list[tuple]] = {}
|
|
113
|
+
for path in sorted(glob.glob(os.path.join(directory, '*.asm'))):
|
|
114
|
+
name = os.path.splitext(os.path.basename(path))[0]
|
|
115
|
+
result[name] = assemble_file(path)
|
|
116
|
+
return result
|
broadcast.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optional OP_RETURN broadcast for BTC-Clocked VM.
|
|
3
|
+
Requires: pip install bit
|
|
4
|
+
Uses Bitcoin testnet by default. Pass network='mainnet' for real BTC.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from bit import PrivateKeyTestnet, PrivateKey
|
|
9
|
+
BIT_AVAILABLE = True
|
|
10
|
+
except ImportError:
|
|
11
|
+
BIT_AVAILABLE = False
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def check_available():
|
|
15
|
+
if not BIT_AVAILABLE:
|
|
16
|
+
raise RuntimeError(
|
|
17
|
+
"The 'bit' library is required for broadcast.\n"
|
|
18
|
+
"Install it with: pip install bit"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_key(wif: str, network: str = 'testnet'):
|
|
23
|
+
check_available()
|
|
24
|
+
if network == 'mainnet':
|
|
25
|
+
return PrivateKey(wif)
|
|
26
|
+
return PrivateKeyTestnet(wif)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def broadcast_commitment(commitment_hex: str, wif: str, network: str = 'testnet') -> str:
|
|
30
|
+
"""
|
|
31
|
+
Broadcast an OP_RETURN transaction embedding commitment_hex (64 hex chars = 32 bytes).
|
|
32
|
+
Returns the transaction hash.
|
|
33
|
+
"""
|
|
34
|
+
check_available()
|
|
35
|
+
key = load_key(wif, network)
|
|
36
|
+
key.get_unspents() # force fresh UTXO fetch including mempool
|
|
37
|
+
# Pass hex string directly; bit encodes as UTF-8, producing 64 ASCII bytes in OP_RETURN.
|
|
38
|
+
# fee=2 sat/vbyte — low priority, safe for non-urgent OP_RETURN commitments.
|
|
39
|
+
# Overrides the fee API default (which spikes to 72+ when the API is unreachable).
|
|
40
|
+
tx_hash = key.send([], message=commitment_hex[:64], fee=2)
|
|
41
|
+
return tx_hash
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def bump_fee(wif: str, fee: int, network: str = 'testnet') -> str:
|
|
45
|
+
"""
|
|
46
|
+
CPFP fee bump for a stuck broadcast: spends this wallet's own pending
|
|
47
|
+
(even unconfirmed) change output in a brand-new transaction at a
|
|
48
|
+
higher fee rate. Since the new tx spends an output from the stuck
|
|
49
|
+
parent, miners can only collect the higher combined fee by confirming
|
|
50
|
+
both together — that's what pulls the stuck tx along with it.
|
|
51
|
+
|
|
52
|
+
Not BIP125 RBF (replacing the stuck tx outright) — `bit`'s send()
|
|
53
|
+
doesn't expose sequence-number control for that. CPFP doesn't need
|
|
54
|
+
the original tx to have signaled anything though, just a spendable
|
|
55
|
+
output still sitting in this same wallet, so it works regardless.
|
|
56
|
+
|
|
57
|
+
Returns the new transaction hash.
|
|
58
|
+
"""
|
|
59
|
+
check_available()
|
|
60
|
+
key = load_key(wif, network)
|
|
61
|
+
key.get_unspents() # includes mempool — this is what makes CPFP possible
|
|
62
|
+
if not key.unspents:
|
|
63
|
+
raise RuntimeError(
|
|
64
|
+
'No spendable UTXOs (confirmed or unconfirmed) for this wallet — '
|
|
65
|
+
'nothing to bump (or the stuck tx already confirmed)'
|
|
66
|
+
)
|
|
67
|
+
return key.send([], fee=fee)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_balance(wif: str, network: str = 'testnet') -> int:
|
|
71
|
+
"""Return balance in satoshis."""
|
|
72
|
+
check_available()
|
|
73
|
+
key = load_key(wif, network)
|
|
74
|
+
# bit's get_balance() returns a string despite the amount always being
|
|
75
|
+
# a whole number of satoshis — cast so this function's own -> int
|
|
76
|
+
# contract is actually true, not just a type hint that happens to lie.
|
|
77
|
+
return int(key.get_balance('satoshi'))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def generate_key(network: str = 'testnet'):
|
|
81
|
+
"""Generate a fresh wallet key for broadcasting commitments. Returns the
|
|
82
|
+
bit key object — caller reads .to_wif() and .address off it."""
|
|
83
|
+
check_available()
|
|
84
|
+
return PrivateKey() if network == 'mainnet' else PrivateKeyTestnet()
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: btcvm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A minimal register machine that executes in lockstep with Bitcoin blocks
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: bitcoin,register-machine,vm,blockchain,vdf,cryptography
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Topic :: Security :: Cryptography
|
|
13
|
+
Classifier: Topic :: System :: Emulators
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
21
|
+
Provides-Extra: s3
|
|
22
|
+
Requires-Dist: boto3>=1.34; extra == "s3"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# btcvm
|
|
26
|
+
|
|
27
|
+
A minimal register machine that executes in lockstep with Bitcoin blocks.
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
## Why
|
|
32
|
+
|
|
33
|
+
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.
|
|
34
|
+
|
|
35
|
+
`btcvm` demonstrates the minimal architecture for a Bitcoin-clocked state machine:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
Bitcoin block hash
|
|
39
|
+
↓
|
|
40
|
+
VM executes N cycles
|
|
41
|
+
↓
|
|
42
|
+
commitment = SHA256(block_hash + state_hash)
|
|
43
|
+
↓
|
|
44
|
+
OP_RETURN → committed to Bitcoin (optional)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
## Files
|
|
50
|
+
|
|
51
|
+
| File | Description |
|
|
52
|
+
|------|-------------|
|
|
53
|
+
| `vm.py` | Minimal register machine (8 registers, 7 opcodes) |
|
|
54
|
+
| `clock.py` | Bitcoin block clock via blockstream.info API |
|
|
55
|
+
| `programs.py` | Sample programs (fibonacci, countdown) |
|
|
56
|
+
| `vdf.py` | VDF sub-clock — sequential SHA256 chain with tick/verify (v1.2) |
|
|
57
|
+
| `trace.py` | Step-by-step execution trace with Merkle root commitment (v2) |
|
|
58
|
+
| `fleet.py` | N parallel VMs with fleet Merkle root — one OP_RETURN anchors all (v3) |
|
|
59
|
+
| `broadcast.py` | Optional OP_RETURN broadcast via the `bit` library |
|
|
60
|
+
| `main.py` | Orchestrator — runs clock loop, executes VM(s), writes ledger |
|
|
61
|
+
| `verify.py` | Verifies ledger, VDF chain, trace, and fleet Merkle against Bitcoin |
|
|
62
|
+
| `ott.py` | Bitcoin-anchored media archive — images, video, and git repos; also an interactive shell |
|
|
63
|
+
| `test_vm.py` | Unit tests (VM, VDF, trace, fleet) |
|
|
64
|
+
| `test_ott_completion.py` | Unit tests (ott shell tab completion, path handling) |
|
|
65
|
+
| `Makefile` | Common tasks: install, test, lint, run, ott |
|
|
66
|
+
| `completions/` | Bash and Zsh tab completions for `btcvm` and `ott` |
|
|
67
|
+
|
|
68
|
+
## VM
|
|
69
|
+
|
|
70
|
+
The VM has 8 registers (`R0`–`R7`) and 7 opcodes:
|
|
71
|
+
|
|
72
|
+
| Opcode | Description |
|
|
73
|
+
|--------|-------------|
|
|
74
|
+
| `LOAD r, val` | Load immediate value into register |
|
|
75
|
+
| `ADD dst, a, b` | `Rdst = Ra + Rb` |
|
|
76
|
+
| `SUB dst, a, b` | `Rdst = Ra - Rb` |
|
|
77
|
+
| `MUL dst, a, b` | `Rdst = Ra * Rb` |
|
|
78
|
+
| `JMP addr` | Unconditional jump |
|
|
79
|
+
| `JZ r, addr` | Jump if register is zero |
|
|
80
|
+
| `HALT` | Stop execution |
|
|
81
|
+
|
|
82
|
+
Programs are tuples loaded into memory. See `programs.py` for examples.
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
**Requirements:** Python 3.11+, no runtime dependencies (stdlib only).
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Install
|
|
90
|
+
pip install -e . # editable install
|
|
91
|
+
make install # same via Makefile
|
|
92
|
+
make dev # with dev deps (pytest, ruff)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### btcvm clock
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# v1 — one entry per Bitcoin block
|
|
99
|
+
python3 main.py fibonacci
|
|
100
|
+
|
|
101
|
+
# v1.2 — VDF sub-clock: 5 ticks per block
|
|
102
|
+
python3 main.py fibonacci --vdf-ticks 5
|
|
103
|
+
|
|
104
|
+
# v2 — trace Merkle root replaces state_hash
|
|
105
|
+
python3 main.py fibonacci --trace
|
|
106
|
+
|
|
107
|
+
# v3 — fleet of 4 parallel VMs, single fleet root per OP_RETURN
|
|
108
|
+
python3 main.py fibonacci --vms 4
|
|
109
|
+
|
|
110
|
+
# Everything combined
|
|
111
|
+
python3 main.py fibonacci --vms 4 --vdf-ticks 3 --trace
|
|
112
|
+
|
|
113
|
+
# Optional: broadcast commitment as OP_RETURN
|
|
114
|
+
pip install bit
|
|
115
|
+
python3 main.py fibonacci --broadcast --wif <your-WIF-key> --network mainnet
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Getting a wallet for broadcast:**
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from bit import PrivateKey
|
|
122
|
+
k = PrivateKey()
|
|
123
|
+
print(k.to_wif(), k.address)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Fund the address with ~5000 sats (covers ~8 commitments at low fee rates).
|
|
127
|
+
|
|
128
|
+
**Verifying a ledger:**
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
python3 verify.py # block hashes + commitments
|
|
132
|
+
python3 verify.py --trace-file trace.jsonl # include trace verification
|
|
133
|
+
python3 verify.py --check-txs # also fetch block tx lists
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### ott — Bitcoin-anchored media archive
|
|
137
|
+
|
|
138
|
+
`ott` stores images and video in a content-addressed Merkle tree and commits
|
|
139
|
+
the root to Bitcoin via the btcvm ledger. Images are hashed whole; video files
|
|
140
|
+
are split into 256 KB chunks with a per-file Merkle tree, enabling byte-range
|
|
141
|
+
inclusion proofs.
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
image.jpg → SHA256 ──────────────────────────────────┐
|
|
145
|
+
├─→ global root → Bitcoin
|
|
146
|
+
video.mp4 → [chunk₀, chunk₁, …, chunkₙ] → file root ┘
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# Stage files (images or video — detected by extension). Hashed now — that's
|
|
151
|
+
# what identifies the file and catches duplicates — but not yet copied into
|
|
152
|
+
# object storage or written to the manifest.
|
|
153
|
+
python3 ott.py add photo.jpg family.jpg video.mp4
|
|
154
|
+
|
|
155
|
+
# Show what's staged vs. already archived, and the current Merkle root
|
|
156
|
+
python3 ott.py status
|
|
157
|
+
|
|
158
|
+
# Changed your mind about a staged file? Drop it before it's archived.
|
|
159
|
+
# (Only works pre-commit — once a file's archived, rm can't touch it.)
|
|
160
|
+
python3 ott.py rm family.jpg
|
|
161
|
+
|
|
162
|
+
# Archive everything staged, then commit the Merkle root to the btcvm
|
|
163
|
+
# ledger (sync is a plain alias for commit — same thing, either name)
|
|
164
|
+
python3 ott.py commit
|
|
165
|
+
|
|
166
|
+
# List all archived files
|
|
167
|
+
python3 ott.py list
|
|
168
|
+
|
|
169
|
+
# Generate a wallet key, then anchor the commitment on-chain (optional)
|
|
170
|
+
python3 ott.py keygen --network testnet
|
|
171
|
+
python3 ott.py broadcast --wif <WIF_KEY>
|
|
172
|
+
|
|
173
|
+
# Verify a file is in the archive (Merkle inclusion proof)
|
|
174
|
+
python3 ott.py verify photo.jpg
|
|
175
|
+
|
|
176
|
+
# Prove a specific 256 KB chunk of a video is in the archive
|
|
177
|
+
python3 ott.py verify-chunk video.mp4 3
|
|
178
|
+
|
|
179
|
+
# Verify every ledger commit against real Bitcoin, not just internal state
|
|
180
|
+
python3 ott.py verify-chain
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Interactive shell:**
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
python3 ott.py # or: python3 ott.py shell
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The shell wraps the same archive with a stateful `cd`-able hierarchy, tab
|
|
190
|
+
completion on every command (archive names, tags, local paths), and a few
|
|
191
|
+
things the flat CLI doesn't have:
|
|
192
|
+
|
|
193
|
+
| Command | Description |
|
|
194
|
+
|---|---|
|
|
195
|
+
| `add [-r] <file>...` | Stage files (git-index style) — hashed now, archived on `commit`/`sync` |
|
|
196
|
+
| `rm <name_or_hash>` | Unstage a pending `add`; refuses once a file's actually committed |
|
|
197
|
+
| `commit` / `sync` | Archive everything staged, then commit the Merkle root to the ledger (same action, either name) |
|
|
198
|
+
| `l [-a] [-t tag] [-b] [dir]` | One-level `ls`-style view of the archive hierarchy (from `orig_path`) |
|
|
199
|
+
| `tree [-a] [-t tag] [-dN] [-b] [dir]` | Recursive tree view; `-d0` for unlimited depth |
|
|
200
|
+
| `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 |
|
|
201
|
+
| `list` / `ls [pattern]` | Full flat dump of every path in one table, optionally filtered by regex (bare or `/slash-delimited/`) |
|
|
202
|
+
| `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) |
|
|
203
|
+
| `mv <name> <new_path>` | Update an entry's tracked path; moves into an existing dir like real `mv` |
|
|
204
|
+
| `reindex [root]` | One indexed filesystem scan — relocates every stale entry and re-anchors `orig_path` to the archive root |
|
|
205
|
+
| `tag <add\|rm\|list> <pattern> <tagname>` | Bulk-tag entries by regex match against their archive path |
|
|
206
|
+
| `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 |
|
|
207
|
+
| `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 |
|
|
208
|
+
| `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" |
|
|
209
|
+
| `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 |
|
|
210
|
+
| `verify-chain [-c]` | Verify every ledger commit against real Bitcoin (refetches each block's actual hash) — `-c` also checks the OP_RETURN landed |
|
|
211
|
+
| `keygen [--network]` | Generate a wallet key for `broadcast` (prints WIF + a QR of the address only, never the key) |
|
|
212
|
+
| `broadcast [--wif]` | Broadcast a commitment as a Bitcoin OP_RETURN tx |
|
|
213
|
+
| `qr [hash\|file]` | QR code for a hash, a file's SHA256, or the current Merkle root |
|
|
214
|
+
| `!<cmd>` | Run a real shell command without leaving the shell |
|
|
215
|
+
|
|
216
|
+
Entries missing at their last known path are hidden by default in `l`/`tree`
|
|
217
|
+
(`-a` shows them); `ott reindex` is the real fix.
|
|
218
|
+
|
|
219
|
+
`add` only stages — nothing's copied into object storage or written to the
|
|
220
|
+
manifest until `commit`/`sync`. Names/hashes resolve relative to the current
|
|
221
|
+
archive directory first (`cd`-aware), only falling back to a global search
|
|
222
|
+
if nothing matches locally.
|
|
223
|
+
|
|
224
|
+
**Via Makefile:**
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
make add FILE=photo.jpg
|
|
228
|
+
make verify-file FILE=photo.jpg
|
|
229
|
+
make ott-status
|
|
230
|
+
make ott-list
|
|
231
|
+
make ott-commit
|
|
232
|
+
make ott-clean # remove manifest + ledger
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Environment variables:**
|
|
236
|
+
|
|
237
|
+
| Variable | Default | Description |
|
|
238
|
+
|---|---|---|
|
|
239
|
+
| `OTT_MANIFEST` | `ott_manifest.jsonl` | Manifest path |
|
|
240
|
+
| `OTT_LEDGER` | `ott_ledger.jsonl` | Ledger path |
|
|
241
|
+
| `OTT_CHUNK_BYTES` | `262144` (256 KB) | Video chunk size |
|
|
242
|
+
| `OTT_HOME` | `~/.ott` | Archive root when not using a local `.ott/` dir |
|
|
243
|
+
| `OTT_BACKEND` | `local` | Storage backend — `local` or `s3` (needs `pip install btcvm[s3]`) |
|
|
244
|
+
| `OTT_S3_BUCKET` | — | Required when `OTT_BACKEND=s3` |
|
|
245
|
+
| `OTT_S3_PREFIX` | `''` | Optional key prefix within the bucket |
|
|
246
|
+
| `OTT_S3_CACHE_DIR` | `.ott/cache` | Local cache dir for S3-backed objects |
|
|
247
|
+
| `OTT_BACKFILL_WORKERS` | `8` | Default concurrency for `backfill`/`verify-objects` |
|
|
248
|
+
|
|
249
|
+
**Proof chain for a video chunk:**
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
chunk N bytes → SHA256 → chunk hash
|
|
253
|
+
↓ Merkle proof (steps: log₂ chunks)
|
|
254
|
+
file root (= global leaf)
|
|
255
|
+
↓ Merkle proof (steps: log₂ files)
|
|
256
|
+
global root → committed to Bitcoin
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`verify-chunk` outputs all three levels: bytes-on-disk match, per-file proof, global proof.
|
|
260
|
+
|
|
261
|
+
## Architecture
|
|
262
|
+
|
|
263
|
+
### VDF sub-clock (v1.2)
|
|
264
|
+
|
|
265
|
+
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.
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
Bitcoin block hash B_h
|
|
269
|
+
tick 0: SHA256ᴺ(B_h) → vdf₀ → VM cycles → commit₀
|
|
270
|
+
tick 1: SHA256ᴺ(vdf₀) → vdf₁ → VM cycles → commit₁
|
|
271
|
+
…
|
|
272
|
+
Next block B_{h+1} reseeds the chain
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Trace commitment (v2)
|
|
276
|
+
|
|
277
|
+
Every VM step is recorded as a hash-chained entry:
|
|
278
|
+
`step_hash = SHA256(prev_hash ‖ pc ‖ op ‖ regs_before ‖ regs_after)`.
|
|
279
|
+
A binary Merkle tree over all step hashes produces a single root that
|
|
280
|
+
replaces `state_hash` in the ledger commitment. Any individual step can
|
|
281
|
+
be verified without replaying the full execution. The trace file is
|
|
282
|
+
the witness a ZK prover (RISC Zero, SP1, Cairo) would consume.
|
|
283
|
+
|
|
284
|
+
### Fleet Merkle root (v3)
|
|
285
|
+
|
|
286
|
+
N VMs run in parallel each tick. A binary Merkle tree over all N
|
|
287
|
+
commitments produces a single `fleet_root` — one OP_RETURN regardless
|
|
288
|
+
of fleet size.
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
tick N:
|
|
292
|
+
VM₀ → commitment₀ ─┐
|
|
293
|
+
VM₁ → commitment₁ ─┤ Merkle → fleet_root → SHA256(block_hash:fleet_root) → OP_RETURN
|
|
294
|
+
VM₂ → commitment₂ ─┤
|
|
295
|
+
VM₃ → commitment₃ ─┘
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Ledger formats
|
|
299
|
+
|
|
300
|
+
**v1 entry:**
|
|
301
|
+
```json
|
|
302
|
+
{
|
|
303
|
+
"block_height": 961224,
|
|
304
|
+
"block_hash": "000000000000...",
|
|
305
|
+
"vdf_tick": 0,
|
|
306
|
+
"vm_ticks": 10,
|
|
307
|
+
"halted": false,
|
|
308
|
+
"registers": [1, 1, 1, 20, 1, 0, 0, 0],
|
|
309
|
+
"state_hash": "a3f9c2b1...",
|
|
310
|
+
"commitment": "88d6f091..."
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**v1.2 additions** (VDF active):
|
|
315
|
+
```json
|
|
316
|
+
{
|
|
317
|
+
"vdf_tick": 2,
|
|
318
|
+
"vdf_input": "prev_tick_output...",
|
|
319
|
+
"vdf_hash": "this_tick_output...",
|
|
320
|
+
"commitment": "SHA256(block_hash:vdf_hash:state_hash)"
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**v2 additions** (trace active):
|
|
325
|
+
```json
|
|
326
|
+
{
|
|
327
|
+
"trace_root": "merkle_root_over_all_steps...",
|
|
328
|
+
"trace_steps": 42,
|
|
329
|
+
"commitment": "SHA256(block_hash:trace_root)"
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**v3** (fleet active) replaces per-VM fields with:
|
|
334
|
+
```json
|
|
335
|
+
{
|
|
336
|
+
"fleet_size": 4,
|
|
337
|
+
"fleet_root": "merkle_root_over_vm_commitments...",
|
|
338
|
+
"commitment": "SHA256(block_hash:fleet_root)",
|
|
339
|
+
"vms": [
|
|
340
|
+
{"vm_id": 0, "program": "fibonacci", "halted": false, "registers": [...], "state_hash": "..."},
|
|
341
|
+
...
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**ott ledger entry:**
|
|
347
|
+
```json
|
|
348
|
+
{
|
|
349
|
+
"ts": "2026-08-17T20:37:00Z",
|
|
350
|
+
"block_height": 961301,
|
|
351
|
+
"block_hash": "000000000000...",
|
|
352
|
+
"merkle_root": "b699603c...",
|
|
353
|
+
"commitment": "9dc75674...",
|
|
354
|
+
"image_count": 3
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Tab completions
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
make completion
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Installs Bash and Zsh completions for both `btcvm` and `ott`:
|
|
365
|
+
- `btcvm` — flags and values (`--vdf-ticks`, `--vms`, etc.)
|
|
366
|
+
- `ott add` — any file
|
|
367
|
+
- `ott verify` — filenames from the manifest
|
|
368
|
+
- `ott verify-chunk` — video names, then chunk indices from the manifest
|
|
369
|
+
|
|
370
|
+
For Zsh, add to `~/.zshrc` if not already present:
|
|
371
|
+
```zsh
|
|
372
|
+
fpath=(~/.zsh/completions $fpath)
|
|
373
|
+
autoload -Uz compinit && compinit
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Makefile targets
|
|
377
|
+
|
|
378
|
+
```
|
|
379
|
+
make install pip install -e .
|
|
380
|
+
make dev install with dev deps
|
|
381
|
+
make test run pytest
|
|
382
|
+
make lint ruff check
|
|
383
|
+
make fix ruff --fix
|
|
384
|
+
make run btcvm one block
|
|
385
|
+
make run-vdf btcvm with VDF sub-clock
|
|
386
|
+
make run-trace btcvm with trace Merkle
|
|
387
|
+
make run-fleet btcvm fleet of 4 VMs
|
|
388
|
+
make verify verify ledger.jsonl
|
|
389
|
+
make ott-status show archive status
|
|
390
|
+
make ott-list list archived files
|
|
391
|
+
make ott-commit commit Merkle root
|
|
392
|
+
make ott-clean remove manifest + ledger
|
|
393
|
+
make ott-repo-add update archived repo record to HEAD
|
|
394
|
+
make ott-tag [OTT_NEXT_TAG=v1.x] sign a git tag + record fingerprint in ott
|
|
395
|
+
make ott-push [OTT_NEXT_TAG=v1.x] push commits + tag, then commit ott root
|
|
396
|
+
make ott-snapshot repo-add + commit root (no tag, no push)
|
|
397
|
+
make ott-release [OTT_NEXT_TAG=v1.x] full: tag → push → commit root
|
|
398
|
+
make add FILE=… add file to ott
|
|
399
|
+
make verify-file FILE=… verify inclusion
|
|
400
|
+
make completion install shell completions
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Roadmap
|
|
404
|
+
|
|
405
|
+
- ✅ **v1** — Bitcoin-clocked register machine, local ledger, optional OP_RETURN
|
|
406
|
+
- ✅ **v1.1** — Ledger verification against block hashes and OP_RETURN
|
|
407
|
+
- ✅ **v1.2** — VDF sub-clock (`--vdf-ticks N`)
|
|
408
|
+
- ✅ **v2** — Trace commitment / Merkle root (`--trace`)
|
|
409
|
+
- ✅ **v3** — Fleet Merkle: N parallel VMs, single OP_RETURN (`--vms N`)
|
|
410
|
+
- ✅ **ott** — Bitcoin-anchored media archive with chunked video and inclusion proofs
|
|
411
|
+
|
|
412
|
+
## Tests
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
make test
|
|
416
|
+
# or
|
|
417
|
+
python3 -m pytest -v
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## License
|
|
421
|
+
|
|
422
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
asm.py,sha256=jLU64mV03od8W32tO0LLg4zIJdVsSYQdrKyGU-UTD-M,3390
|
|
2
|
+
broadcast.py,sha256=0InxvENGObussh84H5yTN5yHSgfpO-q7R3G_rwcVMHg,3195
|
|
3
|
+
clock.py,sha256=bUC8kf684wLylhvIPceyqBi6kuvBjjpihDTS_eUcDzM,1253
|
|
4
|
+
fleet.py,sha256=IYxfcYFUr_SXSdQxz06NLlqw1dYB5KDGQdP-KwIn1Xs,4125
|
|
5
|
+
imgfs.py,sha256=r0UVFJQzhCLl0q1N-GdBUkO8E3nfwBBod_ywu-Eub-s,21208
|
|
6
|
+
main.py,sha256=LcDUA3CHNEsjoK7YuQxvDv577OxS5SUgcdyajSbckqw,13235
|
|
7
|
+
ott.py,sha256=hfFktmEyYndolgSnqNtB0zuVYLRfmTmyVK05J65lXZY,158359
|
|
8
|
+
programs.py,sha256=wfKfNS5PIR4tJZQ-BABfXsF8YyhXCc2fH9Ftw7dTVLs,48
|
|
9
|
+
trace.py,sha256=J3uZ7bC_ht59FEFip_2tg10rjHGopzyNIA5IDATr49I,3669
|
|
10
|
+
vdf.py,sha256=NO2RHYLIfyjLdhXJ-FDHjaXMF_UAad1dcQO1ii3DfR4,2317
|
|
11
|
+
verify.py,sha256=Rm1GL_HVaa8ZM4bIWBoEQyQc_quWbRftlTQ8zA-YgXM,9199
|
|
12
|
+
vm.py,sha256=wpbhh_pe-EG_-hIjYBQqeu_tVJa04ph5XCZ4dXcYic0,2531
|
|
13
|
+
btcvm-0.1.0.dist-info/licenses/LICENSE,sha256=FUDfNJY_4C07UMvXAxlgAv1cVChwTslr4r8nP7mUsyk,1070
|
|
14
|
+
btcvm-0.1.0.dist-info/METADATA,sha256=1mydfdp1tAKlgAK2NhvPnv_2Xkxk-M5My7viWtGufGw,15998
|
|
15
|
+
btcvm-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
16
|
+
btcvm-0.1.0.dist-info/entry_points.txt,sha256=m-UCLYvisqvWCgXmvBBZLmMpeSpcjAxWAPdZ6EQhWyA,51
|
|
17
|
+
btcvm-0.1.0.dist-info/top_level.txt,sha256=NNtbYx-50Ja53p96tPLaWLrWEh6DUB4n6Py7EHvv9qo,70
|
|
18
|
+
btcvm-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ryan Scarbery
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|