Event-sourced, peer-to-peer data store that converges over a partitioning Reticulum/LoRa mesh. No servers, no internet.
Project description
Meshlogd
An event-sourced, eventually-consistent data store that converges over a frequently-partitioning, LoRa radio mesh.
Built for the "trusted collaborators, no internet, private radio" case: a base camp and field teams (humanitarian assessment, SAR, expeditions) collecting a shared dataset with no server and no connectivity, reconciling whenever radios come into range.
The transport is Reticulum on real hardware (LoRa/packet radio/anything), but the engine is transport-agnostic and ships with an in-memory transport so you can run, test, and watch it converge with no radios at all.
Install
pip install meshlogd # core engine (stdlib-only)
pip install "meshlogd[radio]" # + Reticulum, for the real LoRa/packet-radio transport
Naming: the distribution and daemon command are
meshlogd; the importable library ismeshlog(import meshlog) — the trailingdfollows the Unix daemon convention (cf.ssh/sshd), like howpyyamlinstalls but imports asyaml.
The idea in one breath
KoboToolbox already solves offline data entry but syncs to a central server over the internet. meshlog replaces that central sync with peer-to-peer anti-entropy gossip over a radio mesh, so the shared dataset converges with no server and no internet. Kobo (or any form UI) owns the forms; meshlog owns the replication layer Kobo doesn't have.
Why event sourcing makes this easy
The whole database is a grow-only set of immutable, per-author events. That single decision hands you the hard distributed-systems properties almost for free:
- Merging is set union. Union is commutative, associative, and idempotent, so the dataset is a CRDT: it converges regardless of the order or grouping in which events arrive across a partitioning network.
- Per-author contiguous feeds (the Secure-Scuttlebutt trick) collapse "what do you have?" down to one integer per author — a high-water-mark frontier. That digest is tiny enough to ride inside a Reticulum announce.
- Hash-chained feeds make history tamper-evident (we're trusted-collaborator, so we want integrity — just not anonymity).
- The materialized view is a pure fold over the event set, so two nodes with the same events compute byte-identical state. Conflicts on mutable scalar fields resolve by Last-Writer-Wins keyed on a Hybrid Logical Clock, not wall time and not arrival order — so concurrent edits from two islands pick the same winner everywhere.
Anti-entropy protocol (the entire thing)
when two peers can talk:
each sends its FRONTIER = {author: high_water_seq}
on receiving a peer's frontier:
push the slice of every feed above the peer's marks (chunked)
ingest is idempotent + gap-buffering, so overlaps/reorders are harmless
Multi-hop convergence is automatic: a node relays every author's events it holds, not just its own, so A's events reach C through B even if A and C never meet. That's the store-and-forward property, achieved without special-casing it.
Layout
meshlog/
hlc.py Hybrid Logical Clock
event.py immutable, hash-chained, content-addressed Event
feedstore.py append-only per-author log; frontier + delta computation
reducer.py deterministic fold -> materialized records (Kobo-style)
transport.py Transport interface + in-memory SimTransport (partitions)
node.py Node + the anti-entropy protocol
rns_transport.py real Reticulum transport (Link + Resource + announce)
demo_sim.py base camp + 4 teams converge across staged partitions
test_convergence.py 200 randomized partition trials + fold-determinism check
meshlog/cli.py CLI to run a node over a live Reticulum mesh (the `meshlogd` command)
Run it (no radios needed)
python -m demo_sim # scripted scenario, prints the converged dataset
python -m test_convergence # 200 randomized partition/merge trials
The demo islands three groups, has each collect independently, stages a conflicting concurrent edit, then heals the network in stages and verifies every node ends with identical frontiers, identical views, and the same LWW winner.
Run it for real (over Reticulum)
pip install "meshlogd[radio]" # pulls in Reticulum (rns)
# Machine A:
meshlogd --name base --data base.jsonl --create R-100 site_assessment
# Machine B (same LAN to start; add a LoRa interface in ~/.reticulum/config later):
meshlogd --name team_a --data team_a.jsonl --create R-200 water_point
# type `view` at either prompt to watch the dataset converge
Verification status
feedstore,reducer,hlc,node,transport, the demo, and the 200 randomized trials run and pass as committed.rns_transport.py/meshlog/cli.pyare written against the Reticulum 1.3.x API (Identity, Destination, Link, Resource, Packet, announce handlers) and are faithful to the docs/examples, but have not been exercised on a live mesh here. Stand up two instances to validate end-to-end before field use.
Deliberately left as extension points
- Attachments / blobs. Events carry a content hash + size; the bytes are not inlined. Add a content-addressed blob store and transfer blobs lazily over RNS Resources, after structured events, so photos never starve assessment data on a slow link.
- Signatures.
Event.is_authentic()currently checks the content hash. With RNS you already hold each author's Ed25519 key — sign the event id and verify on ingest to close the trusted-but-verify loop. - Wire codec. Prototype uses JSON for readability. Swap
_encode/_decodefor MessagePack or CBOR to cut bytes on the radio. - Compaction / snapshots. Grow-only logs grow. Add periodic signed snapshots
- feed truncation once a quorum has acknowledged a watermark.
- Kobo bridge. Poll the KoboToolbox/ODK submission API (or read KoboCollect
instances) and translate each submission into
record.create+field.setevents. That's the actual on-ramp to the HHI use case.
Reading list (steal from these)
- Secure Scuttlebutt — append-only per-author feeds + gossip replication for intermittent offline networks. This design is a close cousin.
- CRDTs — why union-based / LWW-register merges converge.
- Kulkarni et al. 2014, Hybrid Logical Clocks — the clock used here.
- Reticulum manual, Understanding + API Reference chapters — Link, Resource, and the announce mechanism.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file meshlogd-0.5.0.tar.gz.
File metadata
- Download URL: meshlogd-0.5.0.tar.gz
- Upload date:
- Size: 35.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c6439937af5a10f57a52a1b1c3e296d888b76a1528edd3313ef41f2307fc8437
|
|
| MD5 |
873fdb4df186101fa33325ba93636c56
|
|
| BLAKE2b-256 |
ba49f19469092db40403d0c0ccea02d099ec0531118a242f7ff08a83b4efebef
|
Provenance
The following attestation bundles were made for meshlogd-0.5.0.tar.gz:
Publisher:
publish.yml on meshlogd/meshlogd
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
meshlogd-0.5.0.tar.gz -
Subject digest:
c6439937af5a10f57a52a1b1c3e296d888b76a1528edd3313ef41f2307fc8437 - Sigstore transparency entry: 2010808577
- Sigstore integration time:
-
Permalink:
meshlogd/meshlogd@f5b0760ea1a3ad19ba2e079a8e3e4b0a4633c579 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/meshlogd
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f5b0760ea1a3ad19ba2e079a8e3e4b0a4633c579 -
Trigger Event:
release
-
Statement type:
File details
Details for the file meshlogd-0.5.0-py3-none-any.whl.
File metadata
- Download URL: meshlogd-0.5.0-py3-none-any.whl
- Upload date:
- Size: 37.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a4f0328662d413e56b3cc032139eb24705fbdaab0cb1f0457f9bdcf3a7b3ba6
|
|
| MD5 |
71f161d6b1d340dfda2d5e95672aa9d7
|
|
| BLAKE2b-256 |
f1941ff7a100645e42784091e195f5cb7437a982ef71b85290edcf78a2172dfc
|
Provenance
The following attestation bundles were made for meshlogd-0.5.0-py3-none-any.whl:
Publisher:
publish.yml on meshlogd/meshlogd
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
meshlogd-0.5.0-py3-none-any.whl -
Subject digest:
5a4f0328662d413e56b3cc032139eb24705fbdaab0cb1f0457f9bdcf3a7b3ba6 - Sigstore transparency entry: 2010808626
- Sigstore integration time:
-
Permalink:
meshlogd/meshlogd@f5b0760ea1a3ad19ba2e079a8e3e4b0a4633c579 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/meshlogd
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f5b0760ea1a3ad19ba2e079a8e3e4b0a4633c579 -
Trigger Event:
release
-
Statement type: