otskit
OpenTimestamps proofs with nothing but the Python standard library: read and
write .ots files byte for byte, stamp through the public calendars, upgrade
promises to Bitcoin anchors, and check an anchor against a block header.
pip install otskit # or: copy src/otskit anywhere; it has no dependencies
otskit stamp report.pdf # -> report.pdf.ots, a calendar promise
otskit upgrade report.pdf.ots # an hour or two later: the Bitcoin anchor
otskit info report.pdf.ots
otskit verify report.pdf.ots --header <80-byte header hex from your node>
Python 3.10 or later. No third-party packages, no compiled code, no OpenSSL features beyond SHA-256: RIPEMD-160 and Keccak-256 fall back to plain-Python implementations when the OpenSSL build lacks them (OpenSSL 3 without the legacy provider has no RIPEMD-160, and every Bitcoin-anchored proof uses it).
Why another OpenTimestamps library
The official client works, and this is not meant to replace it. It needs a local Bitcoin Core to verify, and its library is LGPL; the JavaScript library has not been released since 2021. otskit is:
- dependency-free — a verifier can be copied onto a machine with nothing but Python and audited in an afternoon (about 1,700 lines, half of them explanation);
- upstream-compatible, and tested as such — every example proof from the
official repositories is read, written back byte for byte, and compared
attestation by attestation with what the official
python-opentimestampslibrary reads;otskit infoprints exactly whatots infoprints; - explicit about what is verified — see below.
What verify checks, and what it does not
verify recomputes the file's digest, walks every branch of the proof, and for
each Bitcoin attestation compares the message it arrives at with the merkle
root in a block header:
| Header source | Flag | What a verified means |
|---|---|---|
you, from your own node: bitcoin-cli getblockheader <hash> false |
--header HEX |
your node agrees |
| an Esplora server (Blockstream, mempool.space, your own) | --esplora URL (off by default, repeatable) |
that server agrees; several servers = several opinions |
| none | — | nothing: the answer is unchecked, and the height and merkle root are printed for you to compare |
For every header used, otskit also checks that it hashes below the target its
own nBits declares, and — for Esplora — that the header hashes to the block
hash the server named for that height.
This is not SPV. No chain of headers is followed from a checkpoint, no difficulty retargeting is checked, and nothing establishes that the header is part of the most-work Bitcoin chain beyond the word of its source. A header with valid-looking proof of work at an attacker-chosen low difficulty would pass the work check. If you need independence from any server, take the header from your own node. Header-chain verification is the next milestone (see Roadmap).
Exit codes (one word on stdout, details on stderr, --json for a report):
| Code | Word | Meaning |
|---|---|---|
| 0 | verified |
every header consulted holds the proof's merkle root |
| 1 | mismatch |
the file is not the stamped one, or a header for the attested height disagrees |
| 2 | unusable |
the proof or a header cannot be read or fetched |
| 3 | pending |
only calendar promises — not yet a proof of time |
| 4 | unchecked |
anchored, but no header source was given |
Stamping and privacy
stamp hashes the file (SHA-256), appends a 16-byte random nonce, hashes again,
and sends only that to each calendar — the same construction as the official
client. The calendar never sees the file's digest; the nonce stays inside the
.ots. The default calendars are the ones the official client uses
(alice, bob, finney); --calendar replaces them.
upgrade only contacts calendars on a list (the defaults plus --calendar):
the URL inside a pending attestation comes from the proof, and a proof is
untrusted input. The previous file is kept as .ots.bak.
Library
from otskit import parse, serialize, evidence, file_digest, stamp_digest
from otskit.bitcoin import Header, check
proof = parse(open("hello-world.txt.ots", "rb").read())
assert proof.digest == file_digest(open("hello-world.txt", "rb").read(), proof.hash_op)
for item in evidence(proof):
print(item.kind, item.height or item.uri, item.msg.hex())
otskit.proof—parse,serialize,evidence,pending_of,replace_pending,stamp_digest,merge,submission;otskit.calendar—submit,upgrade(urllib, 10-second timeouts);otskit.bitcoin—Header,check,esplora_header,transaction_id;otskit.hashes—sha256,sha1,ripemd160,keccak256.
Limits
- Bitcoin only. Litecoin and the old Ethereum attestations are read, printed and preserved, never verified. Unknown attestations are preserved byte for byte.
- No header-chain (SPV) verification; see above.
- Parsing limits follow the official library: 256 levels of nesting, 4096-byte
messages, 8192-byte attestation payloads. Real proofs run to about 100
levels (
bitcoin.pdf.otsis 101). stampsends one request per file per calendar; it does not batch many files into one merkle tree asots stampdoes.
Tests
pip install pytest && pytest # offline, ~5 seconds
pytest -m network # also asks the real Esplora servers
tests/vectors/ holds the upstream example proofs with expected values from
the official library, three of them upgraded against the live calendars, and
the real block headers of every anchored height (see
tests/vectors/upstream/PROVENANCE.md).
Roadmap
- Header-chain verification without a full node (opentimestamps-client issue #93, open since 2018): checkpoints, headers from several independent sources, linkage, proof of work and retargeting, optionally from genesis.
- A shared test-vector format for all OpenTimestamps implementations, proposed upstream.
- Calendar monitoring: availability and time-to-anchor of the public calendars.
Origin and licence
otskit was extracted from Evix, where it timestamps batches of attestations in Bitcoin; it was rewritten from the format description and does not contain code from the LGPL OpenTimestamps libraries.
Copyright 2026 Lelimo Capital sp. z o.o. Licensed under either of Apache License, Version 2.0 or MIT license at your option. Unless you explicitly state otherwise, any contribution you submit for inclusion shall be dual licensed as above, without additional terms.
Metadata
Release files for otskit 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| otskit-0.1.0.tar.gz | 81.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| otskit-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 114.4 kB
Release files / otskit-0.1.0.tar.gz
| Download URL | otskit-0.1.0.tar.gz |
|---|---|
| Size | 81.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4ca89b8059fde1a41f6abb9d75181cb81ccb1cdcb8f9e95ecab4c7725f5faed0
|
|
BLAKE2b-256 checksum How to use checksums |
e504fd3f2def3df605b624f075925da44624b0b43e8afe2c7b3de679b15825dc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / otskit-0.1.0-py3-none-any.whl
| Download URL | otskit-0.1.0-py3-none-any.whl |
|---|---|
| Size | 32.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2fb9e4f03ed5c0560b20b740d625d8bede24fd6bc06f0098ac7f765f5ca95999
|
|
BLAKE2b-256 checksum How to use checksums |
462f75d81e552ec5a34ccf41ad9462b851e3780f1255eb0b462a22ed0c341b8c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for otskit 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.