Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ a.out
/test_clientid
/test_stamp
/test_map
/test_sha1
/test_sha1_runtime_endian
/test_uuid
/test_elementid
/test_element

# Tooling
Expand Down
42 changes: 33 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ test-string: string.c test_string.c test_util.h
./test_string

.PHONY: test-counter
test-counter: arena.c string.c hashtable.c clientid.c host_posix.c counter.c test_counter.c test_util.h
$(CC) $(CFLAGS) -o test_counter arena.c string.c hashtable.c clientid.c host_posix.c counter.c test_counter.c
test-counter: arena.c string.c hashtable.c clientid.c elementid.c uuid.c sha1.c host_posix.c counter.c test_counter.c test_util.h
$(CC) $(CFLAGS) -o test_counter arena.c string.c hashtable.c clientid.c elementid.c uuid.c sha1.c host_posix.c counter.c test_counter.c
./test_counter

.PHONY: test-scalar
Expand All @@ -47,20 +47,44 @@ test-scalar: arena.c string.c host_posix.c scalar.c test_scalar.c test_util.h
./test_scalar

.PHONY: test-register
test-register: arena.c string.c clientid.c host_posix.c stamp.c scalar.c register.c test_register.c test_util.h
$(CC) $(CFLAGS) -o test_register arena.c string.c clientid.c host_posix.c stamp.c scalar.c register.c test_register.c
test-register: arena.c string.c clientid.c elementid.c uuid.c sha1.c host_posix.c stamp.c scalar.c register.c test_register.c test_util.h
$(CC) $(CFLAGS) -o test_register arena.c string.c clientid.c elementid.c uuid.c sha1.c host_posix.c stamp.c scalar.c register.c test_register.c
./test_register

.PHONY: test-map
test-map: arena.c string.c hashtable.c clientid.c host_posix.c stamp.c scalar.c register.c counter.c element.c map.c test_map.c test_util.h
$(CC) $(CFLAGS) -o test_map arena.c string.c hashtable.c clientid.c host_posix.c stamp.c scalar.c register.c counter.c element.c map.c test_map.c
test-map: arena.c string.c hashtable.c clientid.c elementid.c uuid.c sha1.c host_posix.c stamp.c scalar.c register.c counter.c element.c map.c test_map.c test_util.h
$(CC) $(CFLAGS) -o test_map arena.c string.c hashtable.c clientid.c elementid.c uuid.c sha1.c host_posix.c stamp.c scalar.c register.c counter.c element.c map.c test_map.c
./test_map

.PHONY: test-element
test-element: arena.c string.c hashtable.c clientid.c host_posix.c stamp.c scalar.c register.c counter.c map.c element.c test_element.c test_util.h
$(CC) $(CFLAGS) -o test_element arena.c string.c hashtable.c clientid.c host_posix.c stamp.c scalar.c register.c counter.c map.c element.c test_element.c
test-element: arena.c string.c hashtable.c clientid.c elementid.c uuid.c sha1.c host_posix.c stamp.c scalar.c register.c counter.c map.c element.c test_element.c test_util.h
$(CC) $(CFLAGS) -o test_element arena.c string.c hashtable.c clientid.c elementid.c uuid.c sha1.c host_posix.c stamp.c scalar.c register.c counter.c map.c element.c test_element.c
./test_element

.PHONY: test-elementid
test-elementid: string.c clientid.c elementid.c uuid.c sha1.c host_posix.c test_elementid.c test_util.h
$(CC) $(CFLAGS) -o test_elementid string.c clientid.c elementid.c uuid.c sha1.c host_posix.c test_elementid.c
./test_elementid

.PHONY: test-sha1
test-sha1: sha1.c test_sha1.c test_util.h
$(CC) $(CFLAGS) -o test_sha1 sha1.c test_sha1.c
./test_sha1

# Exercises sha1.c's runtime endian-detection fallback by forcing the
# SHA1_USE_RUNTIME_ENDIAN path at compile time. Independent of whether the
# host's system headers happen to predefine BYTE_ORDER. Must produce
# byte-identical digests to the default build.
.PHONY: test-sha1-runtime-endian
test-sha1-runtime-endian: sha1.c test_sha1.c test_util.h
$(CC) $(CFLAGS) -DSHA1_USE_RUNTIME_ENDIAN -o test_sha1_runtime_endian sha1.c test_sha1.c
./test_sha1_runtime_endian

.PHONY: test-uuid
test-uuid: uuid.c sha1.c test_uuid.c test_util.h
$(CC) $(CFLAGS) -o test_uuid uuid.c sha1.c test_uuid.c
./test_uuid

.PHONY: test-clientid
test-clientid: string.c clientid.c host_posix.c test_clientid.c test_util.h
$(CC) $(CFLAGS) -o test_clientid string.c clientid.c host_posix.c test_clientid.c
Expand All @@ -72,4 +96,4 @@ test-stamp: string.c clientid.c host_posix.c stamp.c test_stamp.c test_util.h
./test_stamp

.PHONY: test
test: test-arena test-hashtable test-string test-counter test-scalar test-register test-clientid test-stamp test-map test-element
test: test-arena test-hashtable test-string test-counter test-scalar test-register test-clientid test-stamp test-map test-sha1 test-sha1-runtime-endian test-uuid test-elementid test-element
11 changes: 7 additions & 4 deletions counter.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,26 @@
#include "host.h"

struct Counter {
ElementId id;
Arena *arena;
HashTable *entries; // client_id (uint32_t) -> CounterEntry
HashTable *entries; // ClientId -> CounterEntry
};

static inline uint32_t max_u32(uint32_t a, uint32_t b) {
if (a > b) {
return a;
}

return b;
}

Counter *counter_create(Arena *arena) {
Counter *counter_create(Arena *arena, ElementId id) {
Counter *counter = arena_alloc(arena, sizeof(Counter));
if (!counter) {
host_abortf(
"counter_create: arena OOM (requested %zu bytes for Counter)",
sizeof(Counter));
}
counter->id = id;
counter->arena = arena;
counter->entries = hashtable_create(arena);
if (!counter->entries) {
Expand All @@ -32,6 +33,8 @@ Counter *counter_create(Arena *arena) {
return counter;
}

ElementId counter_id(const Counter *counter) { return counter->id; }

// Get-or-create the per-client CounterEntry for `client_id`. Initializes a
// fresh entry to {inc=0, dec=0} on first call for a given client.
static CounterEntry *counter_entry_for(Counter *counter, ClientId client_id) {
Expand Down Expand Up @@ -110,7 +113,7 @@ void counter_dec(Counter *counter, ClientId client_id, uint32_t amount) {
}

Counter *counter_clone(Arena *arena, const Counter *counter) {
Counter *clone = counter_create(arena);
Counter *clone = counter_create(arena, counter->id);
HashTableIter it = hashtable_iter(counter->entries);
const void *key;
size_t key_len;
Expand Down
14 changes: 10 additions & 4 deletions counter.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
#define _CRDT_COUNTER_H

// PN-Counter: integer counter with concurrent increments and decrements.
// Identity is positional: a Counter is "the Counter for this slot" via the
// (key, kind) tuple of the containing Map. The Counter struct itself holds
// no identifier.
// Identity: each Counter is stamped with an ElementId at create, exposed
// via counter_id. Two replicas independently creating "the same Counter
// at the same slot" derive identical ids via
// elementid_derive(parent.id, key, key_len, ELEMENT_COUNTER), which is what
// map_merge's recursive guard uses to know they refer to the same logical
// element.
//
// Semantics:
// - Per-client (inc, dec) tallies, one CounterEntry per ClientId that
Expand All @@ -26,6 +29,7 @@

#include "arena.h"
#include "clientid.h"
#include "elementid.h"
#include "hashtable.h"
#include <stdint.h>

Expand All @@ -37,7 +41,9 @@ typedef struct CounterEntry {

typedef struct Counter Counter;

Counter *counter_create(Arena *arena);
Counter *counter_create(Arena *arena, ElementId id);

ElementId counter_id(const Counter *counter);

int64_t counter_read(const Counter *counter);

Expand Down
14 changes: 14 additions & 0 deletions element.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@
#include "register.h"
#include "scalar.h"

ElementId element_id(Element e) {
switch (e.kind) {
case ELEMENT_SCALAR:
host_abort("element_id: scalar elements have no id");
break;
case ELEMENT_REGISTER:
return register_id(e.as.reg);
case ELEMENT_COUNTER:
return counter_id(e.as.counter);
case ELEMENT_MAP:
return map_id(e.as.map);
}
}
Comment thread
vieiralucas marked this conversation as resolved.

Element element_scalar(Scalar s) {
Element e = {.kind = ELEMENT_SCALAR, .as.scalar = s};
return e;
Expand Down
2 changes: 2 additions & 0 deletions element.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
// putting them in the same arena as the containing Map).

#include "counter.h"
#include "elementid.h"
#include "register.h"
#include "scalar.h"

Expand All @@ -46,6 +47,7 @@ typedef struct Element {
} as;
} Element;

ElementId element_id(Element e);
Comment thread
vieiralucas marked this conversation as resolved.
Element element_scalar(Scalar s);
Element element_register(Register *r);
Element element_counter(Counter *c);
Expand Down
49 changes: 49 additions & 0 deletions elementid.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#include "elementid.h"

ElementId elementid_from_bytes(const uint8_t bytes[16]) {
ElementId id;
for (int i = 0; i < 16; i++) {
id.uuid.bytes[i] = bytes[i];
}
return id;
}

ElementId elementid_root(void) {
ElementId id;
for (int i = 0; i < 16; i++) {
id.uuid.bytes[i] = 0;
}
return id;
}

bool elementid_eq(ElementId a, ElementId b) {
for (int i = 0; i < 16; i++) {
if (a.uuid.bytes[i] != b.uuid.bytes[i]) {
return false;
}
}
return true;
}

int elementid_cmp(ElementId a, ElementId b) {
for (int i = 0; i < 16; i++) {
if (a.uuid.bytes[i] < b.uuid.bytes[i]) {
return -1;
} else if (a.uuid.bytes[i] > b.uuid.bytes[i]) {
return 1;
}
}
return 0;
}

ElementId elementid_derive(ElementId parent, const void *key, size_t key_len,
uint8_t kind) {
UuidV5Ctx ctx = {0};
uuid_v5_init(&ctx, parent.uuid.bytes);
uuid_v5_update(&ctx, key, key_len);
uuid_v5_update(&ctx, &kind, sizeof(kind));

ElementId derived = {0};
derived.uuid = uuid_v5_final(&ctx);
return derived;
}
40 changes: 40 additions & 0 deletions elementid.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#ifndef _CRDT_ELEMENTID_H
#define _CRDT_ELEMENTID_H

// ElementId: stable identity of a composite element (Register / Counter /
// Map), shared across replicas. Stamped on the composite at create, never
// mutated afterwards.
//
// Wire format: 16-byte UUID per RFC 4122 / RFC 9562. Convergent derivation
// uses UUID v5 (SHA-1 over namespace + name). The version/variant bits are
// set per spec so the result is a valid UUID — useful for cross-language
// interop, debugging, and standard tooling.
//
// Two replicas independently calling elementid_derive with matching
// inputs land on the same UUID by construction. That's how map_merge's
// recursive guard knows two slots refer to the same logical element.
//
// Manual construction (elementid_from_bytes) is supported for imports and
// for cases where the app provides its own convergence guarantee.

#include "uuid.h"
Comment thread
vieiralucas marked this conversation as resolved.
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

typedef struct ElementId {
UuidV5 uuid;
} ElementId;

ElementId elementid_from_bytes(const uint8_t bytes[16]);

ElementId elementid_root(void);

bool elementid_eq(ElementId a, ElementId b);

int elementid_cmp(ElementId a, ElementId b);

ElementId elementid_derive(ElementId parent, const void *key, size_t key_len,
uint8_t kind);

#endif // _CRDT_ELEMENTID_H
33 changes: 26 additions & 7 deletions map.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@ typedef struct MapEntry {
} Entry;

struct Map {
ElementId id;
Arena *arena;
HashTable *entries;
};

Map *map_create(Arena *arena) {
Map *map_create(Arena *arena, ElementId id) {
Map *map = arena_alloc(arena, sizeof(Map));
if (!map) {
host_abortf("map_create: arena OOM (requested %zu bytes for Map)",
sizeof(Map));
}
map->id = id;
map->arena = arena;
map->entries = hashtable_create(arena);
if (!map->entries) {
Expand All @@ -29,6 +31,8 @@ Map *map_create(Arena *arena) {
return map;
}

ElementId map_id(const Map *map) { return map->id; }

bool map_get(const Map *map, const void *key, size_t key_len, Element *out) {
void *entry;
bool present = hashtable_get(map->entries, key, key_len, &entry);
Expand Down Expand Up @@ -124,11 +128,23 @@ void map_merge(Map *dst, const Map *src) {
Entry *de;
bool dst_has = hashtable_get(dst->entries, k, klen, (void **)&de);

// Recursive: both alive, same composite kind and same id then
// element_merge. This wins over slot LWW.
// Same key, both alive composites of same kind: either recurse in
// place (matching ids → same logical element) or LWW-clone the
// winner (mismatched ids → distinct logical elements that happen
// to share the slot).
if (dst_has && !de->is_tombstone && !se->is_tombstone &&
de->value.kind == se->value.kind &&
de->value.kind != ELEMENT_SCALAR) {
if (!elementid_eq(element_id(de->value), element_id(se->value))) {
// Distinct logical elements at the same slot. LWW the
// slot; if src wins, replace dst's composite with a
// clone of src's. Loser is orphaned.
if (stamp_gt(se->stamp, de->stamp)) {
de->value = element_clone(dst->arena, se->value);
de->stamp = se->stamp;
}
continue;
}
element_merge(de->value, se->value);
// Advance slot stamp to max(dst, src) so future slot-level
// ops on this key are LWW-deterministic across replicas.
Expand Down Expand Up @@ -182,7 +198,8 @@ Counter *map_counter(Map *map, const void *key, size_t key_len, Stamp stamp) {
return existing->value.as.counter;
}

Counter *fresh = counter_create(map->arena);
ElementId id = elementid_derive(map->id, key, key_len, ELEMENT_COUNTER);
Counter *fresh = counter_create(map->arena, id);
if (!present || stamp_gt(stamp, existing->stamp)) {
map_set(map, key, key_len, element_counter(fresh), stamp);
}
Expand All @@ -201,7 +218,8 @@ Register *map_register(Map *map, const void *key, size_t key_len, Scalar seed,
return existing->value.as.reg;
}

Register *fresh = register_create(map->arena, seed, stamp);
ElementId id = elementid_derive(map->id, key, key_len, ELEMENT_REGISTER);
Register *fresh = register_create(map->arena, id, seed, stamp);
if (!present || stamp_gt(stamp, existing->stamp)) {
map_set(map, key, key_len, element_register(fresh), stamp);
}
Expand All @@ -217,15 +235,16 @@ Map *map_map(Map *map, const void *key, size_t key_len, Stamp stamp) {
return existing->value.as.map;
}

Map *fresh = map_create(map->arena);
ElementId id = elementid_derive(map->id, key, key_len, ELEMENT_MAP);
Map *fresh = map_create(map->arena, id);
if (!present || stamp_gt(stamp, existing->stamp)) {
map_set(map, key, key_len, element_map(fresh), stamp);
}
return fresh;
}

Map *map_clone(Arena *arena, const Map *map) {
Map *clone = map_create(arena);
Map *clone = map_create(arena, map->id);
HashTableIter it = hashtable_iter(map->entries);
const void *k;
size_t klen;
Expand Down
Loading