Skip to main content

matrym-hashchain

PyPI CI codecov Python License Dependencies

A tamper-evident, append-only, hash-chained ledger. Dependency-free (stdlib only).

Each entry carries a SHA-256 over its own payload and the previous entry's hash, so any later edit, reorder, or deletion of a past record is detected the next time the log is read. It's a file-simple, zero-dependency primitive for audit trails, transaction logs, and chain-of-custody records - the kind of thing a compliance, finance, or records system needs and shouldn't hand-roll.

Install

pip install matrym-hashchain

Released to PyPI via trusted publishing (GitHub OIDC, no stored token).

Use

from pathlib import Path
from hashchain import append, read, verify, HashChainError

ledger = Path("audit.jsonl")

append(ledger, {"event": "created", "who": "alice"})
append(ledger, {"event": "approved", "who": "bob"})

for entry in read(ledger):          # read() verifies the whole chain as it goes
    print(entry.seq, entry.payload)

verify(ledger)                      # True -- the history reads clean end to end

# If anyone edits, reorders, or deletes a past record...
ledger.write_text(ledger.read_text().replace("alice", "mallory"))
verify(ledger)                      # False
read(ledger)                        # raises HashChainError: record 0 was tampered ...

The store is plain JSON Lines (.jsonl) - one record per line, human-readable, greppable, and portable. No database, no daemon, no dependencies.

Command line

The same three verbs, from one terminal line (installed as matrym-hashchain, or run python -m hashchain):

matrym-hashchain append audit.jsonl '{"event": "created", "who": "alice"}'
matrym-hashchain append audit.jsonl '{"event": "approved", "who": "bob"}'
matrym-hashchain read   audit.jsonl        # 0<TAB>{"event": "created", ...}
matrym-hashchain verify audit.jsonl        # "clean: 2 record(s), head e7c1f28a3595"; exit 0

verify exits 0 on a clean chain and 1 on a broken one, naming the tampered record on stderr, so it drops straight into a shell gate or a CI step. append also reads the payload object from stdin when you omit it (... | matrym-hashchain append audit.jsonl).

Signing and truncation work from the CLI too. The HMAC key is read from an environment variable (never the command line, so it can't leak into the process table or shell history):

export LEDGER_KEY='a secret only writers hold'
matrym-hashchain append audit.jsonl '{"event": "created"}' --key-env LEDGER_KEY
matrym-hashchain verify audit.jsonl --key-env LEDGER_KEY          # exit 0; without it -> exit 1

anchor=$(matrym-hashchain head audit.jsonl --key-env LEDGER_KEY)  # stash off-ledger
matrym-hashchain verify audit.jsonl --key-env LEDGER_KEY --expected-head "$anchor"  # catches a dropped tail

What it guarantees (and what it doesn't)

This proves integrity by default, and closes the two classic gaps with two optional features:

Attack Detected?
A payload was edited ✅ content-hash mismatch
Records were reordered or one was inserted ✅ sequence / prior-hash mismatch
A middle record was deleted ✅ the next record's prior-hash no longer links
The last record(s) were dropped ✅ with an anchor - stash head_hash(path) elsewhere (a receipt, a second store, a witness) and check verify(path, expected_head=...); the chain alone can't see truncation
Who wrote a record (authenticity) ✅ with a signing key - pass key=... to sign records with HMAC-SHA256; only a key-holder can produce or validate them

Any break fails loud with HashChainError rather than returning a dishonest history.

Signing and truncation, honestly

KEY = b"a secret only writers hold"
append(ledger, {"event": "created"}, key=KEY)   # HMAC-signed
verify(ledger, key=KEY)                          # True; verify(ledger) alone -> False (no key)

anchor = head_hash(ledger, key=KEY)              # stash this off-ledger
# ... later, after someone drops the last record ...
verify(ledger, key=KEY, expected_head=anchor)    # False -- the tail is gone

Honest label: HMAC is a symmetric shared-secret MAC. It proves a record was written by a holder of the key; it is not public-key non-repudiation (anyone with the key can both sign and verify). The same key must be passed to every append/read/verify/head_hash over a signed ledger.

One more honest bound: this is a single-writer primitive. append is not guarded against two processes writing the same ledger at once (no file lock), so concurrent appends can interleave. Serialize writers (one process, or your own lock) if that's a risk; read/verify are read-only and safe alongside a single writer.

Performance

append is O(1) in ledger size: it reads only the ledger's tail (a backward seek), not the whole chain, so appends stay fast as the log grows without bound. That claim ships with evidence, not just an assertion, measured head to head against the pre-0.1.1 O(n) behavior:

ledger size O(n) baseline O(1) current speedup
500 7.15 ms 0.08 ms 93x
64,000 930.52 ms 0.08 ms 11,941x

Across a 128x ledger growth the old append slowed 130x (linear, ~1 s per write at 64k records); the current one stayed flat at ~0.08 ms. Full method, table, and reproduction: benchmarks/RESULTS.md (make bench).

API

All functions take an optional keyword-only key: bytes (sign/verify with HMAC; omit for plain integrity).

  • append(path, payload, *, key=None) -> Entry - validate, hash-chain, and append one record. Reads only the tail (O(1)), validating that record before extending, so you can't quietly append onto a tampered tail.
  • read(path, *, key=None) -> list[Entry] - read every record, verifying as it goes. Empty/missing store -> [].
  • verify(path, *, key=None, expected_head=None) -> bool - True if the ledger reads clean end to end; pass expected_head to also catch truncation.
  • head_hash(path, *, key=None) -> str - the last record's content hash (or GENESIS if empty); the value you anchor off-ledger to detect truncation.
  • content_hash(mapping, *, key=None) -> str - the canonical digest the chain uses: SHA-256 unkeyed, HMAC-SHA256 when a key is given (sorted keys, no whitespace).
  • Entry(seq, payload, prior_hash, content_hash) - one link; HashChainError - the loud failure.

Test

pip install -e ".[dev]"   # pytest, ruff, mypy, coverage, bandit, pip-audit
make check                # ruff + mypy --strict + pytest at 100% coverage (the full gate)
make security             # bandit SAST + pip-audit the dependency closure (proves "zero deps")
make bench                # the O(1)-append benchmark

The suite pins both acceptance and refusal: a clean chain reads end to end and verify returns True, while every tampered case (an edited payload, a reorder, a deleted middle record, a mislinked record with a valid own hash, an append onto an already-broken log) fails loud with HashChainError rather than returning a dishonest history. CI runs it on every push across Python 3.10-3.13, at 100% line + branch coverage, and audits the dependency closure.

Evaluation

Scored by forge-audit - the fleet's proof-tool, which runs the gates on a target repo behind a mockable GitHub-API seam and emits a pass|watchlist|fail scorecard. This part passes at the advanced stage (a snapshot; reproduce with forge-audit --path . --stage advanced --online):

Dimension Verdict Evidence
lint ✅ pass clean
typecheck ✅ pass clean
tests ✅ pass green suite, coverage 100% ≥ 85%
security ✅ pass bandit + pip-audit clean
dependencies ✅ pass clean
ci ✅ pass 3 CI workflows (check, release, CodeQL)
collaboration ✅ pass 9 merged PRs
performance ✅ pass benchmark artifact present
readme ✅ pass covers purpose, install, run, test

Overall: pass (advanced). Role signals: testing, security, backend, devops, collaboration, performance, documentation.

Provenance

This is a MatrymLabs Hardware Store part: a pattern first proven in the CodeForge MUD engine (the "Chronicle" - the game's tamper-evident memory), then reused across the fleet (a federal guidance-check ledger; an AI-triage decision audit trail). Published standalone so any project can reuse the same tested primitive - one well-made part, many jobs.

License

MIT

Metadata

Release files for matrym-hashchain 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for matrym-hashchain 0.4.0
File Size Uploaded Provenance Actions
matrym_hashchain-0.4.0.tar.gz 21.3 kB With provenance Details for matrym_hashchain-0.4.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for matrym-hashchain 0.4.0
File Interpreter ABI Platform Actions
matrym_hashchain-0.4.0-py3-none-any.whl Python 3 none any Details for matrym_hashchain-0.4.0-py3-none-any.whl

Total release size: 36.1 kB

Release files / matrym_hashchain-0.4.0.tar.gz

Download URL matrym_hashchain-0.4.0.tar.gz
Size 21.3 kB
Tags Source
SHA-256 checksum
How to use checksums
f3792334d0d80d6d89653bee1dfef303e432876d09c3c8e55b350b202496c37d
BLAKE2b-256 checksum
How to use checksums
e2ca8474c149bfa6e05e57e8a94ba4d063a25795e26478f34b88c318fff4a539
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 23, 2026.

Transparency log

Release files / matrym_hashchain-0.4.0-py3-none-any.whl

Download URL matrym_hashchain-0.4.0-py3-none-any.whl
Size 14.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d2888adf77a16175610321cddd065546519486eb1072a46f62cd3bff63ea6ede
BLAKE2b-256 checksum
How to use checksums
99929d791b570da68a4dd3c7e69a43d7d03a8ff107d54e753fb3d0d5c873a06a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 23, 2026.

Transparency log

Security for matrym-hashchain 0.4.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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for matrym_hashchain-0.4.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 23, 2026.

Transparency log

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.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page