Merkle-CRDT sync
This package provides a synchronization layer for MerkleCRDT instances (from the merkledag package) over a dart_libp2p network. It facilitates the replication of Merkle-CRDTs among multiple peers by:
- Announcing new CRDT heads (root CIDs) using libp2p GossipSub.
- Fetching required Merkle-DAG nodes directly from peers via libp2p streams.
- Utilizing a Kademlia DHT (if available) for discovering peers that can provide specific DAG nodes.
It’s designed to be used in conjunction with the merkledag package for the core Merkle-CRDT logic and dart_libp2p for the underlying peer-to-peer networking.
Features
Section titled “Features”- Merkle-CRDT State Synchronization: Keeps Merkle-CRDT instances eventually consistent across multiple libp2p peers.
- GossipSub for Head Announcements: Efficiently disseminates new CRDT state heads (root CIDs) using
dart_libp2p_pubsub. - Direct P2P Stream-based DAG Syncing: Fetches only the necessary Merkle-DAG nodes directly from peers, minimizing redundant data transfer.
- DHT Provider Discovery: Can leverage a Kademlia DHT (e.g.,
dart_libp2p_kad_dht) to find peers holding specific DAG nodes. - Local Block Storage: Includes an in-memory block store (
InMemoryBlockStore) and an interface (BlockStore) for custom persistent storage solutions. - Centralized Management: Provides a
MerkleCrdtGossipManagerclass to orchestrate the CRDT instance, DAG syncer, and broadcaster. - Generic: Works with any CRDT payload type
Pthat implementsCRDTPayload<V>from themerkledagpackage.
Core Components
Section titled “Core Components”MerkleCrdtGossipManager<V, P>: The main class an application interacts with. It manages aMerkleCRDTinstance and coordinates synchronization.P2PStreamDagFetcher<P, V>: Implements theDAGSyncer<P>interface frommerkledag. Responsible for fetching and storingMerkleNode<P>objects. Uses libp2p streams for direct transfer and can query a DHT for providers.GossipSubAnnouncer: Implements theBroadcasterinterface frommerkledag. Usesdart_libp2p_pubsubto broadcast new head CIDs and subscribe to remote head announcements.BlockStore<P, V>: Interface for storingMerkleNode<P>objects locally.InMemoryBlockStore<P, V>: A simple in-memory implementation.
Getting Started
Section titled “Getting Started”Prerequisites
Section titled “Prerequisites”- A working Dart environment.
- A configured libp2p
Hostinstance fromdart_libp2p. - A
PubSubinstance fromdart_libp2p_pubsub(typically usingGossipSubRouter). - Optionally, an
IpfsDHTinstance fromdart_libp2p_kad_dhtif DHT provider discovery is desired.
Add Dependencies
Section titled “Add Dependencies”Add this package and its peer dependencies to your pubspec.yaml. Since these are likely local/path dependencies during development:
dependencies: # This package dart_libp2p_merkle_crdt: path: [path_to_this_package] # Replace with actual path or version if published
# Core Merkle-CRDT logic merkledag: path: [path_to_merkledag_package] # e.g., /Users/stephanfeb/IdeaProjects/bsv_apps/merkledag
# Libp2p stack dart_libp2p: path: [path_to_dart_libp2p_package] # e.g., /Users/stephanfeb/IdeaProjects/dart-libp2p dart_libp2p_pubsub: path: [path_to_dart_libp2p_pubsub_package] # e.g., /Users/stephanfeb/IdeaProjects/bsv_apps/dart-libp2p-pubsub dart_libp2p_kad_dht: # Optional, for DHT support in P2PStreamDagFetcher path: [path_to_dart_libp2p_kat_dht_package] # e.g., /Users/stephanfeb/IdeaProjects/bsv_apps/dart-libp2p-kat-dht
# Other common dependencies dart_cid: path: [path_to_dart_cid_package] # e.g., /Users/stephanfeb/IdeaProjects/bsv_apps/dart-cid logging: ^1.2.0 # Or your preferred logging package versionRun dart pub get.
Here’s an example of how to set up and use the MerkleCrdtGossipManager with a GSet<String> CRDT.
import 'dart:async';import 'dart:typed_data';
import 'package:dart_libp2p/core/host/host.dart' show Host;import 'package:dart_libp2p/core/peer/peer_id.dart' show PeerId;// Assuming you have set up your libp2p Host, PubSub, and optionally DHT// import your_libp2p_setup_file.dart';
import 'package:merkledag/merkledag.dart' show GSet, CRDTPayload; // For GSet and CRDTPayloadimport 'package:dart_libp2p_merkle_crdt/dart_libp2p_merkle_crdt.dart';import 'package:logging/logging.dart';
// Example Payload Factory for GSet<String>// P (Payload Type) is GSet<String>// V (Value Type) is Set<String>GSet<String> gsetPayloadFactory(Uint8List payloadBytes) { // This is a simplified example. GSet would need a proper fromBytes constructor. // For now, assuming it can be reconstructed or this factory is more complex. // If GSet.fromCanonicalBytes() existed: return GSet.fromCanonicalBytes(payloadBytes); // For this example, let's assume it's empty if bytes are unrecognized. // In a real scenario, GSet would need a proper deserialization method. _logger.warning('gsetPayloadFactory: Deserialization from Uint8List is a placeholder.'); return GSet<String>(); // Placeholder: returns an empty GSet}
// Placeholder for your libp2p Host, PubSub, and DHT instanceslate Host myHost;late PubSub myPubSub;IpfsDHT? myDht; // Optional
final _logger = Logger('MerkleCrdtUsageExample');
void main() async { // --- Setup Phase (Illustrative - replace with your actual libp2p setup) --- // 1. Initialize Logger (optional) Logger.root.level = Level.INFO; Logger.root.onRecord.listen((record) { print('${record.level.name}: ${record.time}: ${record.loggerName}: ${record.message}'); if (record.error != null) print('ERROR: ${record.error}, ${record.stackTrace}'); });
// 2. Initialize your libp2p Host, PubSub, and DHT (myHost, myPubSub, myDht) // This part is highly dependent on your application's libp2p setup. // For example, using `createLibp2pNode` from `test/real_net_stack.dart` in this package // or a similar utility in your main application. // myHost = await setupMyLibp2pHost(); // myPubSub = PubSub(myHost, GossipSubRouter()); // Example // await myPubSub.start(); // myDht = await setupMyDht(myHost); // Example // await myDht?.bootstrap(); // For this example to run, these would need to be actual initialized instances. // This example will not run as-is without a concrete libp2p stack. print('Placeholder: Initialize myHost, myPubSub, and optionally myDht here.'); // return; // Uncomment if you want to stop before manager setup without real instances.
// --- MerkleCrdtGossipManager Setup ---
// 3. Create a unique ID for this CRDT instance (e.g., a document ID) // This ID is used to scope GossipSub topics. const crdtInstanceId = 'my-shared-document-123';
// 4. Create the BlockStore final blockStore = InMemoryBlockStore<GSet<String>, Set<String>>();
// 5. Create the P2PStreamDagFetcher (DAGSyncer) // The protocol ID can be any unique string for your DAG sync protocol. final dagFetcher = P2PStreamDagFetcher<GSet<String>, Set<String>>( myHost, blockStore, '/my-app/merkle-crdt-dag-sync/1.0.0', gsetPayloadFactory, // Provide the factory for your payload type P // cidFunction was removed from P2PStreamDagFetcher constructor dht: myDht, // Pass the DHT instance if you have one );
// 6. Create the GossipSubAnnouncer (Broadcaster) final announcer = GossipSubAnnouncer( myHost, myPubSub, crdtInstanceId, // Ensures announcements are scoped to this CRDT );
// 7. Create the MerkleCrdtGossipManager // V = Set<String>, P = GSet<String> final manager = MerkleCrdtGossipManager<Set<String>, GSet<String>>( dagFetcher: dagFetcher, announcer: announcer, );
// --- Using the Manager ---
// 8. Listen to state changes manager.onStateChanged.listen((Set<String>? newState) { _logger.info('CRDT state changed (logical value): $newState'); // Note: onStateChanged emits V?, which is Set<String>? in this case. });
// 9. Apply a local update _logger.info('Applying first local update...'); final update1 = GSet<String>(); update1.add('apple'); update1.add('banana'); await manager.applyLocalUpdate(update1); // Current state (logical value V) will be {'apple', 'banana'} // This will also trigger a broadcast of the new head CID.
// Simulate some time for gossip and potential remote updates await Future.delayed(Duration(seconds: 2));
// 10. Apply another local update _logger.info('Applying second local update...'); final update2 = GSet<String>(); update2.add('cherry'); // Note: GSet merge logic means 'apple' and 'banana' are preserved. await manager.applyLocalUpdate(update2); // Current state (logical value V) will be {'apple', 'banana', 'cherry'}
// 11. Get the current state directly Set<String>? currentState = await manager.getState(); _logger.info('Directly fetched current state (logical value): $currentState');
// 12. Refresh state (useful if MerkleCRDT doesn't have its own state stream for remote changes) // This explicitly fetches the latest state from the underlying MerkleCRDT, // which would include any remote changes processed internally by MerkleCRDT. await manager.refreshState(); _logger.info('State after explicit refresh (logical value): ${manager.currentState}');
// --- Cleanup --- // Depending on your application, you might want to close the manager // when it's no longer needed. This will stop listening for remote heads. // The underlying Host, PubSub, DHT, etc., should be managed separately. // await manager.close(); // await myPubSub.stop(); // await myDht?.close(); // await myHost.close();}Architecture Notes
Section titled “Architecture Notes”MerkleCRDT(frommerkledagpackage): This is the core data structure. It manages the Merkle-DAG of CRDT payloads and their Merkle clock. It uses aDAGSyncerto fetch/store nodes and aBroadcasterto announce/receive heads.P2PStreamDagFetcher(DAGSyncer):- When
MerkleCRDTneeds a node (get(CID)), the fetcher first checks its localBlockStore. - If not found, and if a DHT is provided, it queries the DHT for peers (
AddrInfo) providing that CID. - It then attempts to connect to these peers (or the peer from which a head was announced) and requests the node over a direct libp2p stream using a custom protocol.
- When
MerkleCRDTcreates a new node (put(MerkleNode)), the fetcher stores it in theBlockStoreand (if a DHT is provided) announces itself as a provider for that node’s CID on the DHT.
- When
GossipSubAnnouncer(Broadcaster):- When
MerkleCRDThas a new head (e.g., afteradd(payload)), it callsbroadcast(cidString)on the announcer. The announcer then publishes a small JSON message{ "cid": "...", "senderPeerId": "..." }to a CRDT-instance-specific GossipSub topic. - The
MerkleCRDT(via the announcer passed to its constructor) also callssubscribe()on the announcer. TheMerkleCRDTitself listens to this stream of incoming CID strings and internally processes them (fetching data via theDAGSyncerand merging).
- When
MerkleCrdtGossipManager:- Initializes and holds the
MerkleCRDT,P2PStreamDagFetcher, andGossipSubAnnouncer. - Provides a simplified API (
applyLocalUpdate,getState,onStateChanged) to the application, dealing with the logical value typeV. - Converts between the logical value
Vand the CRDT payloadPwhere necessary (e.g.,P.valuegivesV).
- Initializes and holds the
Important Considerations
Section titled “Important Considerations”MerkleCRDT’s Internal Head Processing: This library assumes that theMerkleCRDTclass from themerkledagpackage, when given aBroadcaster, will internally callbroadcaster.subscribe()and process the incoming CIDs to trigger its merge logic (which in turn uses theDAGSyncer). If this is not the case, theMerkleCrdtGossipManagerwould need to be adjusted to explicitly handle CIDs from the announcer’s stream and call an appropriate method onMerkleCRDTto incorporate them.- Payload Deserialization (
payloadFactory): TheP2PStreamDagFetcherrequires apayloadFactoryfunction (P Function(Uint8List payloadBytes)) to deserializeMerkleNodepayloads received over the network. You must provide a correct factory for your specificCRDTPayloadtypeP. - Serialization/Deserialization of
MerkleNode: The current implementation inP2PStreamDagFetcheruses a simplified JSON-based serialization forMerkleNodeobjects for transmission over P2P streams. For production, consider a more efficient and robust binary format (e.g., Protobuf). - Error Handling & Resilience: The current error handling is basic. Production systems would require more sophisticated retry mechanisms, connection management, and error reporting.
ActivityPub-Inspired P2P Social Primitives (“activity-pubsub”)
Section titled “ActivityPub-Inspired P2P Social Primitives (“activity-pubsub”)”This library also provides a suite of P2P social primitives inspired by ActivityPub, built on top of the core Merkle-CRDT synchronization layer. These primitives, developed under the “activity-pubsub” initiative, enable developers to easily integrate decentralized social features into their applications. They leverage DIDs for identity, IPLD for data structures, Merkle-CRDTs for state management, and libp2p (GossipSub and Bitswap) for communication.
Core Concepts
Section titled “Core Concepts”- Actors: Represent users or entities in the social graph, identified by Decentralized Identifiers (DIDs), typically
did:key. Each actor has a Profile and manages their own data. - Content Objects (IPLD): Social objects like posts (
Note) are structured using IPLD, allowing them to be content-addressed (referenced by CID). - Activities (IPLD): Actions performed by actors (e.g., creating a note, following another actor, liking content) are also modeled as IPLD objects. These activities are signed by the actor’s private key.
- CRDTs for State: Mutable collections and actor states (e.g., profiles, lists of posts in an
outbox,followinglists,likeditems) are managed using Merkle-CRDTs. This ensures eventual consistency across peers without central servers. - Replication & Exchange:
- Updates to CRDT heads (signifying new state) are announced and replicated using the
dart-libp2p-merkle-crdtmechanisms (i.e., GossipSub for head announcements and direct P2P stream-based DAG syncing for CRDT nodes). - IPLD content objects and activities are fetched by their CIDs using a Bitswap-compatible mechanism (facilitated by
P2PStreamDagFetcheror a dedicated Bitswap client).
- Updates to CRDT heads (signifying new state) are announced and replicated using the
Key Features & Usage
Section titled “Key Features & Usage”The following features correspond to Phases 1-3 of the activity-pubsub.md development plan.
1. Actor Identity & Profile
Section titled “1. Actor Identity & Profile”- Actor Identity (
lib/src/p2p/identity/actor.dart):- Actors generate and manage their cryptographic key pairs, from which DIDs (e.g.,
did:key) are derived.
- Actors generate and manage their cryptographic key pairs, from which DIDs (e.g.,
- Actor Profile CRDT (
ProfileCrdt-lib/src/p2p/crdt/profile_crdt.dart):- A Merkle-CRDT (e.g., LWW-Map or JSON-CRDT) storing profile information.
- Required fields:
id(DID),type(“Person”, “Service”, etc.),publicKeyJwk. - Recommended fields:
name,preferredUsername,summary,icon(CID to image),image(CID to image). - Usage:
- An actor creates and updates their
ProfileCrdt. - The head of this CRDT is gossiped (e.g., on a topic like
profiles/<actor_did>). - Clients discover and sync an actor’s
ProfileCrdtto view their profile.
- An actor creates and updates their
2. Content Publishing
Section titled “2. Content Publishing”- Content Object (
Note-lib/src/p2p/ipld/note.dart):- An IPLD schema for simple content (e.g.,
{"@context": "...", "type": "Note", "content": "...", "published": "timestamp"}). - Actors create
NoteIPLD blocks, resulting in a CID.
- An IPLD schema for simple content (e.g.,
CreateActivity (IPLD - schema defined inlib/src/p2p/ipld/activity.dartcontext):- An IPLD schema (e.g.,
{"@context": "...", "type": "Create", "actor": "actor_did", "object": "note_cid", "published": "timestamp"}). - Activities are signed by the actor’s private key.
- Actors create
Createactivity IPLD blocks, resulting in a CID.
- An IPLD schema (e.g.,
outboxCRDT (OutboxCrdt-lib/src/p2p/crdt/outbox_crdt.dart):- An append-only log CRDT storing CIDs of an actor’s activities.
- Usage:
- To publish: An actor appends the CID of a new activity (e.g., a
Createactivity for aNote) to theirOutboxCrdt. - The
OutboxCrdthead is gossiped (e.g., on topicoutboxes/<actor_did>). - Clients sync an actor’s
OutboxCrdtto retrieve their activities, then fetch the actual IPLD objects (Activities, Notes) by CID via Bitswap. - Actors should make their own content and activities available via Bitswap (e.g., by “pinning” them in their local
BlockStore).
- To publish: An actor appends the CID of a new activity (e.g., a
3. Basic Social Interactions
Section titled “3. Basic Social Interactions”FollowActivity &followingCRDT:FollowActivity(lib/src/p2p/ipld/follow_activity.dart): IPLD schema (e.g.,{"type": "Follow", "actor": "follower_did", "object": "followed_actor_did"}). Signed and added to the follower’soutbox.FollowingCrdt(lib/src/p2p/crdt/following_crdt.dart): An OR-Set CRDT storing DIDs of actors being followed.- Usage: To follow someone, an actor adds a
FollowActivityto theiroutboxand updates theirFollowingCrdt. Clients sync this CRDT to know who the user follows.
LikeActivity &likedCRDT:LikeActivity(lib/src/p2p/ipld/like_activity.dart): IPLD schema (e.g.,{"type": "Like", "actor": "liker_did", "object": "liked_content_cid"}). Signed and added to the liker’soutbox.LikedCrdt(lib/src/p2p/crdt/liked_crdt.dart): An OR-Set CRDT storing CIDs of liked content.- Usage: To like content, an actor adds a
LikeActivityto theiroutboxand updates theirLikedCrdt.
4. Enhanced Interactions & Content Management
Section titled “4. Enhanced Interactions & Content Management”- Replies (
CreateActivitywithinReplyTo):- A reply is a
Noteobject (IPLD). TheCreateactivity for the reply includes aninReplyTo: "original_content_cid"property. - The reply activity is signed and added to the replier’s
outbox. - Usage: Clients reconstruct conversation threads by linking
inReplyToproperties. Reply discovery can be client-side (e.g., scanning outboxes of followed users).
- A reply is a
Announce(Boost/Share) Activity (AnnounceActivity-lib/src/p2p/ipld/announce_activity.dart):- IPLD schema (e.g.,
{"type": "Announce", "actor": "announcer_did", "object": "announced_content_cid"}). - Signed and added to the announcer’s
outbox. - Usage: Client displays announced content, attributing the original author and the announcer.
- IPLD schema (e.g.,
DeleteActivity (Tombstoning) (DeleteActivity-lib/src/p2p/ipld/delete_activity.dart):- IPLD schema (e.g.,
{"type": "Delete", "actor": "deleter_did", "object": "content_to_delete_cid"}). - Signed and added to the actor’s
outbox. - Usage:
- Clients should hide content associated with a
Deleteactivity. - The actor’s client should unpin (stop serving via Bitswap) their “deleted” content. Other clients may optionally unpin it from their local cache.
- Clients should hide content associated with a
- IPLD schema (e.g.,
- Actor Mentions:
- Implemented by convention using
Mentiontags withinNotecontent (e.g.,{"type": "Mention", "href": "mentioned_actor_did"}). - Usage: Initial mention discovery is typically client-side, scanning content from followed users.
- Implemented by convention using
Integrating “activity-pubsub” Features into an Application
Section titled “Integrating “activity-pubsub” Features into an Application”- Core Setup:
- Initialize your libp2p
Host,PubSub(GossipSub), and optionallyIpfsDHTas described in the “Getting Started” section of this README.
- Initialize your libp2p
- Manage CRDT Instances:
- For each actor and each type of social CRDT (Profile, Outbox, Following, Liked), you will typically instantiate a
MerkleCrdtGossipManager. - The
crdtInstanceIdfor each manager should be unique and discoverable, often incorporating the actor’s DID and the CRDT type (e.g.,profile-<actor_did>,outbox-<actor_did>). - Provide the appropriate
payloadFactoryfor each CRDT type (e.g.,ProfileCrdt.fromBytes,OutboxCrdt.fromBytes).
- For each actor and each type of social CRDT (Profile, Outbox, Following, Liked), you will typically instantiate a
- Actor Management:
- Implement logic for creating/loading actor identities (key pairs, DIDs).
- Implement Social Actions:
- Creating Content:
- Construct the
NoteIPLD object. Store it locally (e.g., viaP2PStreamDagFetcher’sblockStore.put()) to get its CID and make it available via Bitswap. - Construct the
CreateActivityIPLD object, embedding theNote’s CID. Sign it. Store it and get its CID. - Update the actor’s
OutboxCrdtby adding theCreateActivity’s CID, then callmanager.applyLocalUpdate()on the outbox manager.
- Construct the
- Other Activities (Follow, Like, Announce, Delete): Follow a similar pattern: create the activity IPLD object, sign it, store it, get its CID, and add this CID to the actor’s
OutboxCrdt. ForFollowandLike, also update the respectiveFollowingCrdtorLikedCrdt.
- Creating Content:
- Data Fetching & Synchronization:
- To view a profile, sync the
ProfileCrdtfor the target actor’s DID. - To build a feed or view posts, sync the
OutboxCrdtof relevant actors. For each activity CID in an outbox, fetch the activity IPLD object, and if it’s aCreateorAnnounceof aNote, fetch theNoteIPLD object. This is done via theP2PStreamDagFetcher(which uses Bitswap-like mechanisms).
- To view a profile, sync the
- Client-Side Logic:
- Develop the UI/UX for presenting social information.
- Implement client-side aggregation (e.g., a chronological feed from multiple followed outboxes).
- Handle threading of replies by processing
inReplyTofields. - Respect
Deleteactivities by hiding or removing content.
The “activity-pubsub” primitives provide a flexible foundation. Application developers are responsible for combining these building blocks to create rich user experiences. For more details on the specific CRDTs and IPLD models, refer to the source files in lib/src/p2p/.
Future development (Phase 4 and beyond) aims to explore advanced discovery, storage strategies, moderation primitives, and more.
Additional Information
Section titled “Additional Information”merkledagpackage: [Link to yourmerkledagpackage or its documentation]dart_libp2psuite: [Link todart_libp2pand related packages]
To contribute, please file an issue or submit a pull request.