Skip to content

feat(examples): P2PCalc : decentralised EtherCalc collaboration via GossipSub with HLC + CRDT#36

Open
DZDasherKTB wants to merge 2 commits intoseetadev:mainfrom
DZDasherKTB:feat/p2pcalc-ethercalc-libp2p-integration
Open

feat(examples): P2PCalc : decentralised EtherCalc collaboration via GossipSub with HLC + CRDT#36
DZDasherKTB wants to merge 2 commits intoseetadev:mainfrom
DZDasherKTB:feat/p2pcalc-ethercalc-libp2p-integration

Conversation

@DZDasherKTB
Copy link
Copy Markdown

@DZDasherKTB DZDasherKTB commented May 7, 2026

Overview

This PR adds examples/p2pcalc/, a working integration layer that
replaces EtherCalc's centralised Redis pub-sub synchronisation with
py-libp2p GossipSub. Multiple peers collaboratively edit the same
spreadsheet in real time with no central server, no single point of
failure, and no changes to EtherCalc's source code or browser UI.

Closes #34


The problem with EtherCalc's current architecture

EtherCalc relays SocialCalc commands through a central Node.js server
over Redis pub-sub. Every edit flows:
Browser -> WebSocket -> Node.js -> Redis -> Node.js -> WebSocket -> Browser

This introduces a single point of failure, makes offline-first workflows
impossible, and creates a centralised trust requirement that is
unacceptable for civic-tech and low-connectivity rural deployments.

The key insight from studying EtherCalc's internals: the server does
not compute, it only relays
. Every SocialCalc command is executed
client-side in the browser's JavaScript engine. The server is a message
bus. If we replace that bus with a decentralised one, we get peer-to-peer
collaboration for free.


How the integration works

EtherCalc uses Redis pub-sub channels internally, one per room
(sc:<room_name>). P2PCalc subscribes to this channel, wraps each
command in a typed operation envelope, and broadcasts it via GossipSub
to the topic p2pcalc/<room_name>. Incoming operations from remote
peers are decoded and injected back into the same Redis channel.
EtherCalc's existing Socket.io layer delivers them to browsers unchanged.
Browser -> WS -> Node.js -> Redis ──> P2PCalc Adapter

GossipSub mesh
p2pcalc/

Remote P2PCalc Adapter <── Redis <- Node.js <- WS <- Browser

Zero changes to EtherCalc. Zero changes to the browser UI.


Innovations

Hybrid Logical Clocks (HLC) for causal ordering

Wall-clock timestamps break under clock drift and network partitions.
Pure Lamport clocks lose physical time meaning. P2PCalc uses Hybrid
Logical Clocks (Kulkarni et al., 2014), combining physical time with
a logical counter, to achieve causally-consistent operation ordering
across peers without any clock synchronisation protocol.

Multi-Value Register for conflict resolution

Last-Write-Wins silently loses data. If Alice and Bob edit cell A1
concurrently, one edit disappears with no warning. P2PCalc implements
a Multi-Value Register (MVR) that preserves all concurrent versions of
a cell as candidates. Conflicts are surfaced as cell markers directly
in the spreadsheet UI so users can explicitly resolve them, no silent
data loss. A write that causally follows all candidates (higher HLC)
automatically dominates and resolves the conflict.

Replicated Growable Array for structural operations

Row and column insertions/deletions are harder than cell values.
Operational Transformation handles this with transformation functions
that have known correctness issues for spreadsheet structures. P2PCalc
uses an RGA (Roh et al., 2011) that tracks operation intent rather than
absolute position. Concurrent insertions at the same index are
deterministically ordered by peer_id. Deletions use tombstones so
concurrent edits targeting a deleted row can still be applied correctly.

Causal buffering for out-of-order delivery

GossipSub provides best-effort, eventually-consistent delivery.
Operations may arrive out of causal order after reconnection or late
join. P2PCalc attaches causal dependency (op_id of last observed
operation) to each message. The adapter holds operations in a causal
buffer until their declared dependencies have arrived, then flushes in
dependency order. This gives causal consistency on top of GossipSub's
eventual delivery without vector clocks.

Two-phase late-joiner state sync

On joining a sheet, the peer broadcasts a SNAPSHOT_REQUEST via
GossipSub. Responding peers send SNAPSHOT_CHUNK operations. If
multiple peers respond simultaneously, P2PCalc selects the peer with
the highest operation count (most history) as authoritative, no
coordination required. After the snapshot applies, operations that
arrived after the snapshot are replayed using the embedded
op_log_from field.

Local WAL for crash recovery

Every operation is appended to a length-prefixed MessagePack write-ahead
log. On restart, the peer replays its local WAL before requesting a
network snapshot, recovery works without other peers being online, and
the snapshot request covers only the delta.

MessagePack over JSON

Operations are serialised with MessagePack: 30-40% smaller and 2-3x
faster than JSON. The GossipSub heartbeat interval is set to 1 second
(vs the default 2 seconds) to reduce propagation latency without
saturating the mesh.

Operation integrity verification

Every operation carries a SHA-256 content hash. The GossipSub message
validator rejects operations that fail integrity checks at the pub-sub
layer, before forwarding to other mesh peers.


Files added

examples/p2pcalc/
├── README.md problem, approach, all 8 innovations documented
├── requirements.txt
├── p2pcalc/
│ ├── operation.py SocialCalc command parsing, HLC, MessagePack
│ ├── crdt.py MVR (cell conflicts) + RGA (structural ops)
│ ├── adapter.py Redis <-> GossipSub bridge, causal buffer
│ ├── p2p_node.py libp2p host, GossipSub v2.0, topic-per-sheet
│ ├── state_sync.py snapshot assembly, race resolution, WAL
│ └── main.py CLI: --room, --port, --peer, --conflict-policy
└── tests/
└── test_p2pcalc.py 42 unit tests


How to test (no network or Redis required)

cd examples/p2pcalc
pip install -r requirements.txt
python -m pytest tests/test_p2pcalc.py -v
# Expected: 42 passed

Test coverage: HLC monotonicity and causal ordering, SocialCalc command
parsing for all operation types, MessagePack roundtrip, integrity
verification, MVR conflict detection and resolution across all three
policies, RGA concurrent structural merge, SheetCRDT deduplication and
op-log, snapshot race resolution, multi-peer convergence simulation.


How to run end-to-end

# Terminal 1
python -m p2pcalc.main --room my-sheet --port 4001

# Terminal 2 (paste multiaddr printed by Terminal 1)
python -m p2pcalc.main --room my-sheet --port 4002 \
  --peer /ip4/127.0.0.1/tcp/4001/p2p/<peer_id>

# Open http://localhost:8000/my-sheet in two browser windows
# Edits in either window propagate via GossipSub, no central server

References

  • Kulkarni et al., "Logical Physical Clocks", 2014 (HLC)
  • Shapiro et al., "Conflict-Free Replicated Data Types", INRIA 2011
  • Roh et al., "Replicated Abstract Data Types", 2011 (RGA)
  • Tan, "From SocialCalc to EtherCalc", AOSA Vol. 2, 2012

Author: Dashpreet Singh, dashpreetsinghhanda@gmail.com
IIT Jammu, B.Tech CSE 2024-2028

Adds examples/p2pcalc/ — a working integration layer that replaces
EtherCalc's centralised Redis pub-sub sync with py-libp2p GossipSub.

Architecture:
- operation.py: SocialCalc command encoding with Hybrid Logical Clocks
  and MessagePack serialisation
- crdt.py: Multi-Value Register (cell conflicts) + RGA (structural ops)
- adapter.py: Redis <-> GossipSub bridge (zero EtherCalc source changes)
- p2p_node.py: libp2p host with GossipSub v2.0, topic-per-sheet design
- state_sync.py: two-phase late-joiner snapshot + op-log replay

Innovations beyond baseline:
- HLC timestamps instead of wall clocks (causal ordering across peers)
- MVR conflict detection surfaces concurrent edits rather than silently
  dropping them (unlike LWW)
- RGA for row/col structural ops handles concurrent inserts correctly
- Causal buffering ensures out-of-order ops are applied correctly
- Echo loop prevention at Redis inject layer
- Local op-log WAL for crash recovery without full network resync

Tests: 42 unit tests, no network or Redis required.

Closes seetadev#34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[DMP 2026]: Deep Integration of EtherCalc & SocialCalc UI with py-libp2p for Decentralized Real-Time Collaboration

1 participant