1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
|
# 0. License
Contact `<mailto:colloid@romanilin.is>` to discuss a commercial license if:
* Your use case exceeds the production-use thresholds described in `LICENSE.txt`.
* You wish to embed or distribute the Licensed Work in a proprietary product.
* Your corporate policy prohibits the use of BUSL-1.1 or AGPL-3.0-or-later software and requires a standard commercial software agreement.
Commercial licenses remain available for all versions of the software even after they have transitioned to the Change License.
# 1. Invariants & Guarantees
This section defines the formal promises and explicit limitations of Colloid. Every mechanism described in sections 2–15 must trace back to an invariant defined here. If a mechanism cannot be justified by an invariant, it does not belong in the system.
## 1.1 Notation
| Symbol | Definition |
|:---|:---|
| $N$ | Total nodes in the cluster |
| $D$ | Diameter of the reachable overlay graph |
| $W_K$ | Number of distinct actors (daemons) that have ever written to key $K$ |
| $V_A$ | Version vector (causal context) of state $A$ |
| $A \parallel B$ | Mutations $A$ and $B$ are concurrent: $\neg(V_A \le V_B) \land \neg(V_B \le V_A)$ |
| $V_A \le V_B$ | Component-wise dominance: $\forall i,\ V_A[i] \le V_B[i]$ |
| $\text{dom}(d, V)$ | Dot $d = (\text{actor}, \text{seq})$ is dominated by context $V$: $V[\text{actor}] \ge \text{seq}$ |
## 1.2 Consistency Model
Colloid is an **AP system** under the CAP theorem: it prioritizes Availability and Partition tolerance over strong Consistency during network splits, except "no failover" workloads.
The consistency model is **Strong Eventual Consistency (SEC)**: if any two nodes have received the same set of mutations (regardless of order or duplication), their computed local states are identical.
SEC is a mathematical property of the CRDT merge function (commutativity, associativity, idempotency). It holds unconditionally, regardless of network behavior.
## 1.3 Safety Invariants
**SAFE-1 — Conflict Blocking.** A CRDT key whose MV-Register holds concurrent values is in `CONFLICT` state. No actuator on any node may act on a *configuration* key in `CONFLICT` state. The cluster preserves the current physical reality until a human operator resolves the conflict.
A key $K$ is in `CONFLICT` if and only if $|\text{K.entries}| > 1$. While $K$ is in `CONFLICT`, the actuator treats $K$ as absent from the desired state. Workloads already running under a previous non-conflicting value continue unchanged.
**SAFE-2 — Local Conflict Detection.** Every node detects `CONFLICT` independently by examining the entry count of its local MV-Register. Zero inter-node coordination is required.
**SAFE-3 — Local Resolution Verification.** A resolving mutation $R$ clears `CONFLICT` on key $K$ if and only if $\text{dom}(d_i, V_R)$ for every dot $d_i$ in the MV-Register's entries. Every node verifies this independently.
**SAFE-4 — No Silent Data Loss.** The MV-Register never discards a concurrent value. All concurrent values are preserved and surfaced to the operator. LWW-Registers are explicitly rejected for configuration state.
**SAFE-5 — Stateful Workload Affinity.** A stateful workload's state volume binds its execution to a deterministic cell selection. A stateful workload is never executed concurrently on different cells in the same partition (still possible across partitions per EXEC-1). Replicas of stateful workloads use an explicit volume replication protocol (Section 13.6), not implicit re-scheduling.
**SAFE-6 — Sandbox Determinism.** WebAssembly workloads are executed in a deterministic sandbox: identical bytecode + identical inputs yield identical outputs. Side effects are gated by an explicit host capability set declared in the WorkloadSpec.
## 1.4 Availability Invariants
**AVAIL-1 — Partition Independence.** During a network partition, every connected sub-network continues to accept mutations and actuate non-conflicting desired state.
**AVAIL-2 — Partition Healing.** Upon reconnection, CRDT merge automatically resolves divergent state without human intervention — unless the partition caused concurrent mutations to the same key, which surfaces as `CONFLICT` per SAFE-1.
**AVAIL-3 — No Single Point of Failure.** No master, leader, or coordinator exists. The loss of any individual node does not degrade functionality for surviving nodes.
**AVAIL-4 — Protocol Quiescence.** A cluster with no pending mutations, no active failures, and no in-flight anti-entropy generates zero gossip and probe traffic. Between anti-entropy timer ticks and SWIM probes, a stable cluster is silent on the state plane. NAT keepalives (Section 6.4) are a separate, bounded traffic class.
**AVAIL-5 — NAT Transparency.** Two cells may form a mesh adjacency across symmetric NAT, full-cone NAT, and CGNAT environments, provided at least one publicly reachable cell exists in the cluster to serve as a rendezvous (Section 6).
**AVAIL-6 — Gateway Continuity.** The cluster advertises service IPs (Virtual IPs) such that external traffic continues to reach the service when the previously-advertising cell fails. Cells negotiate gateway responsibility without a central load balancer (Section 14).
## 1.5 Convergence Bounds
**CONV-1 — Topology-Bound Propagation.** State propagation resolves in $O(D)$ overlay hops.
| Physical Topology | Overlay Diameter | Convergence |
|:---|:---|:---|
| Well-connected datacenter | $D \sim \log N$ | $O(\log N)$ |
| Sparse WAN with bottlenecks | $D > \log N$ | $O(D)$ |
| Linear chain | $D = N - 1$ | $O(N)$ |
This is an information-theoretic lower bound.
**CONV-2 — Guaranteed Eventual Convergence.** If the network is eventually connected (every node can transitively reach every other node at some future time), all nodes converge to identical state in finite time. Gossip provides fast propagation; anti-entropy guarantees completeness.
**CONV-3 — Gateway Convergence Bound.** Following a cell failure that held a Virtual IP, external reachability is restored within $T_{\text{gw}} = T_{\text{SWIM}} + T_{\text{ARP}} + T_{\text{BGP}}$, where $T_{\text{SWIM}}$ is the failure detection time (Section 9), $T_{\text{ARP}}$ is the gratuitous ARP propagation time on the local segment, and $T_{\text{BGP}}$ is the BGP withdrawal/advertisement convergence time at the upstream router.
## 1.6 Architectural Boundaries
**ARCH-1 — Traffic Class Separation.** All data belongs to exactly one of two classes:
| Class | Examples | Transport | Scope | Persistence |
|:---|:---|:---|:---|:---|
| State | CRDT mutations (config, health, placement, volume metadata) | Gossip broadcast | All nodes | Replicated |
| Stream | Logs, exec sessions, metrics, volume bytes | Point-to-point | Source → requester | Ephemeral / out-of-band |
No mechanism may store stream data in the CRDT or propagate state data through the streaming path.
*Justification:* This prevents unbounded state growth. If Colloid's CRDT accepted streaming data, replicated state would grow unboundedly; if state mutations were routed point-to-point, convergence would break.
**ARCH-2 — Unified State Ingress.** The sole mechanism for modifying desired cluster state is a CRDT mutation injected into the mesh. No separate API server, configuration endpoint, or management plane exists.
**ARCH-3 — Push-Based Propagation.** Mutations propagate immediately upon injection via gossip. No node polls any other node. Anti-entropy exists solely to repair state missed during failures.
**ARCH-4 — Local Queryability.** Every node holds complete state (RES-3). Querying cluster state generates zero network traffic. `colloid status` reads local memory.
**ARCH-5 — Kernel-Offloaded Data Plane.** Where the Linux kernel provides a verified, programmable in-kernel path (eBPF, XDP, sockmap, sk_lookup), Colloid uses it for the data plane (service VIPs, load distribution, observability counters, packet filtering). The control plane (CRDT, gossip, routing, scheduling) remains in user space. Kernel programs are loaded by the daemon and their state is exposed to the control plane through eBPF maps.
**ARCH-6 — Submission-Queue I/O.** All user-space I/O (UDP, UDS, file, timerfd, eventfd, accept) is submitted through a single `io_uring` instance per daemon. The reactor thread interacts with the kernel through submission and completion queue rings; no `read`/`write`/`recvmsg`/`sendmsg` syscalls are issued from the hot path.
## 1.7 Bootstrap Guarantees
**BOOT-1 — Zero-Configuration Genesis.** A single node forms a functional cluster with no external dependencies, no configuration files, no tokens, and no preexisting state. `colloid` with no arguments is a valid and complete bootstrap.
**BOOT-2 — Single-Contact Join.** Joining an existing cluster requires exactly one network address of any reachable member, or a publicly resolvable rendezvous address (see Section 6). No join tokens, certificates, shared secrets, or configuration files. Authentication during join is deferred to the enterprise extension (NON-5).
**BOOT-3 — Ephemeral Participation.** A CLI tool may join the mesh as a temporary single-peer cell, inject or query state, and depart without affecting cluster health tracking or routing table stability.
**BOOT-4 — NAT-Bound Bootstrap.** A node behind NAT may join the mesh by performing a STUN-style address discovery against any reachable peer (Section 6.2). The discovered observed address is published in the cell's contact record and used by other cells for hole-punched direct connections.
## 1.8 Execution Semantics
**EXEC-1 — At-Least-Once Execution.** If a workload is present in converged, non-conflicting desired state and at least one healthy node exists, the workload will be executed by at least one node. During partitions, the same workload may execute simultaneously in different partitions.
**EXEC-2 — Deterministic Placement.** Given identical inputs (workload set $W$, healthy node set $H$, replica count $R$, runtime class), every node independently computes identical placement. Zero inter-node communication required.
**EXEC-3 — Minimal Disruption on Failure.** When a node is declared dead, Rendezvous hashing redistributes only that node's workloads. Other workloads are unaffected.
**EXEC-4 — Multi-Runtime Workloads.** A workload declares its runtime class: `oci` (containers), `wasm` (WebAssembly), or `process` (host-native binary). The actuator dispatches to the appropriate runtime backend (Section 13). Placement scoring is uniform across runtime classes.
**EXEC-5 — Stateful Anchoring.** A workload declared `stateful` is anchored: its score is biased toward the cell(s) currently holding its state volume (Section 10.5). A stateful workload migrates only when the anchored cell is declared dead, and only after volume re-replication completes on the target cell.
## 1.9 Routing Properties
**ROUTE-1 — Logarithmic Degree.** Each node maintains $O(\log N)$ routing table entries.
**ROUTE-2 — Logarithmic Lookup.** Any key or node can be located in $O(\log N)$ overlay hops in a well-connected topology.
**ROUTE-3 — Graceful Degradation.** Constrained devices may maintain fewer connections (smaller bucket size). Lookup latency increases; correctness is preserved.
**ROUTE-4 — Physical Awareness.** Within the XOR-determined bucket structure, peer selection prefers lower-RTT, higher-reliability, NAT-traversable contacts. Routing respects physical network topology without breaking the mathematical overlay geometry.
**ROUTE-5 — Reachability Classes.** Each contact carries a reachability class: `PUBLIC` (directly addressable), `PUNCHED` (reachable after STUN coordination), `RELAYED` (only reachable through a relay), or `UNKNOWN`. Peer selection prefers `PUBLIC > PUNCHED > RELAYED` after RTT ties are broken.
## 1.10 Stream Guarantees
**STREAM-1 — Target Resolution Is Local.** Resolving which cell(s) host a given workload requires zero network traffic.
**STREAM-2 — Direct When Reachable.** If the target cell is reachable directly (`PUBLIC` or `PUNCHED`), the stream is established without an intermediate node.
**STREAM-3 — Relay When Not Reachable.** If the target cell is only reachable as `RELAYED`, the stream is forwarded through the DHT overlay. Intermediate nodes forward stream packets without inspecting or storing payload content beyond the relay header.
**STREAM-4 — Ordered Delivery.** Stream packets carry a sequence number. The receiver reorders at the application layer.
**STREAM-5 — No Persistence.** Stream data is never written to the CRDT, never persisted on relay nodes (ARCH-1).
**STREAM-6 — Hole-Punched First.** Before falling back to relay, two cells attempt UDP hole punching coordinated through a third cell that is reachable to both (Section 6.3).
## 1.11 Resource Contracts
**RES-1 — No Heap Allocation in Hot Path.** The reactor loop (submit → reap → parse → merge → respond) performs zero `malloc`/`free`/`realloc`/`calloc` calls. All variable-size state is served from pre-allocated arenas sized at startup. `io_uring` SQE/CQE buffers are mmap'd at initialization.
**RES-2 — Bounded Packet Size.** Every UDP datagram is ≤ 1200 bytes, guaranteeing fragmentation-free delivery across common paths including tunnel/VPN encapsulations.
**RES-3 — Full State Replication.** Every node holds a complete replica of the global CRDT state. Any node can answer any query, detect any conflict, or compute placement locally.
For clusters where total CRDT state exceeds available memory, RES-3 becomes the binding constraint on cluster size. Sharding is a future extension that would relax RES-3 and modify ARCH-4, SAFE-2, and EXEC-2. This specification assumes RES-3 holds.
**RES-4 — Probabilistic Node Identity Uniqueness.** Node IDs are 64-bit values. For $N < 2^{32}$:
$$P(\text{collision}) \sim \frac{N^2}{2^{65}}$$
For $N = 100{,}000$, $P \sim 5.4 \times 10^{-11}$.
**RES-5 — Bounded Kernel Footprint.** eBPF programs loaded by Colloid are sized at compile time. Map sizes are sized at startup based on cluster scale parameters. No kernel allocation in the steady state.
## 1.12 CRDT Identity Rule
**CRDT-ACTOR — Actor Identity Binding.** The actor ID in any version vector entry is the Cell ID of the *daemon* that injected the mutation into the mesh. CLI tools and automation agents do not possess their own actor IDs; they delegate to their connected daemon.
*Justification:* Without this rule, each ephemeral CLI invocation generates a new actor ID, causing version vectors to grow with the number of CLI invocations (unbounded). With this rule, version vectors grow with $W_K$, the number of distinct *daemons* that have written to key $K$. In an orchestration system, $W_K$ is small: health keys have $W_K = 1$ (the cell itself); configuration keys have $W_K \le$ the number of operational endpoints (typically 1–5).
## 1.13 Explicit Non-Guarantees
**NON-1 — No Linearizability.** Operations are not globally ordered.
**NON-2 — No Exactly-Once Execution.** Duplicate workload execution is possible during partitions (EXEC-1). For stateful workloads, partition-time concurrent execution may produce divergent state branches that surface as `CONFLICT` on the volume metadata key after healing (Section 10).
**NON-3 — No Bounded Convergence Time.** Convergence depends on network conditions.
**NON-4 — No Causal Message Delivery.** A receiver may observe mutations out of causal order. Correctness is maintained by SEC.
**NON-5 — No Access Control.** Any node reaching the mesh can join and inject mutations. Authentication and RBAC are enterprise extensions.
**NON-6 — No Durable CRDT Storage by Default.** Colloid does not `fsync` CRDT state to disk on every mutation. Nodes recover CRDT state from peers via anti-entropy. Total cluster power-loss is unrecoverable for the CRDT; the operator re-applies configuration from source files (ARCH-2). Volume contents (Section 10) are persisted separately and do `fsync`.
**NON-7 — No L7 Load Balancing.** Colloid provides L3/L4 service VIPs only. HTTP-level routing, TLS termination, and host-header dispatch are out of scope.
**NON-8 — No Wasm Linearizability.** Wasm workloads with capability-granted state access participate in the same CRDT model as native workloads. No additional ordering guarantees beyond SEC.
---
# 2. Node Identity & Lifecycle
*Implements: RES-4, BOOT-1, BOOT-2, BOOT-3, AVAIL-3.*
## 2.1 Cell ID Generation
Each Colloid daemon generates a 64-bit Cell ID on first boot using Xoshiro256\*\*, seeded from the operating system's cryptographic entropy source (`getrandom(2)`).
The ID is generated once and persisted to a local file (`$COLLOID_DIR/cell_id`). Subsequent daemon restarts reuse the persisted ID, preserving the node's identity for version vector continuity.
If no persisted ID exists, the daemon generates a new one.
## 2.2 Cell States
```
BOOTING ──→ ACTIVE ──→ LEAVING ──→ GONE
│
└──(crash)──→ DEAD (declared by peers)
│
└──(restart)──→ BOOTING (resurrection)
```
| State | Description | Entry Condition |
|:---|:---|:---|
| `BOOTING` | Generating ID, performing STUN discovery, contacting seed peer, populating routing table, performing initial anti-entropy. Not yet scheduling. | Daemon start |
| `ACTIVE` | Full mesh participant. Gossips, routes, schedules, actuates, advertises VIPs. | Bootstrap complete |
| `LEAVING` | Graceful shutdown initiated. Sends `LEAVE` to all K-bucket peers. Withdraws BGP advertisements and gratuitous-ARPs VIP reassignment. Stops accepting new workloads. Drains running workloads. | `colloid shutdown` or SIGTERM |
| `GONE` | Daemon process has exited cleanly. Peers received `LEAVE` and exclude from scheduling immediately. | `LEAVE` acknowledged |
| `DEAD` | Declared dead by peers via failure detection (Section 9). The cell did not send `LEAVE`. | SWIM timeout + corroboration |
## 2.3 Ephemeral Cells
The CLI is an ephemeral single-peer cell. It:
1. Does **not** generate a persistent Cell ID. It uses a temporary in-memory ID.
2. Connects to the local daemon via UDS (Section 5.2), not UDP.
3. Reads CRDT state from the daemon's local replica.
4. Injects mutations by instructing the daemon to perform the CRDT write.
5. Departs without sending `LEAVE` on the mesh.
Ephemeral cells are excluded from routing tables, health tracking, scheduling, and tombstone watermark calculations.
## 2.4 Reachability Class Discovery
On bootstrap, the daemon determines its own reachability class by STUN-style probing (Section 6.2):
- **PUBLIC** — observed source address equals bound socket address.
- **PUNCHED** — observed source address differs from bound socket address, but the NAT preserves the mapping across multiple probes (cone-like).
- **RELAYED** — observed source address varies per destination, indicating symmetric NAT; direct reachability impossible.
The reachability class is published in the cell's contact record (Section 4.4) and updated whenever it changes (e.g., upstream reconfiguration).
## 2.5 Collision Handling
During the bootstrap handshake (Section 6.6), if a joining node discovers its generated Cell ID already exists in the cluster, it regenerates the ID and restarts the bootstrap.
---
# 3. Wire Protocol
*Implements: RES-2. Defines the byte-level encoding for all inter-node communication.*
## 3.1 Common Header
Every UDP datagram begins with a 16-byte header:
```
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| version | msg_type | payload_len |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ sender_id +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| msg_nonce |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
| Offset | Size | Field | Description |
|:---|:---|:---|:---|
| 0 | 1 | `version` | Protocol version. `0x01`. |
| 1 | 1 | `msg_type` | Message type (Section 3.2). |
| 2 | 2 | `payload_len` | Length of payload after the header. Big-endian. |
| 4 | 8 | `sender_id` | Cell ID of the sending node. Big-endian. |
| 12 | 4 | `msg_nonce` | Opaque value for ACK correlation and deduplication. Big-endian. |
**Total header: 16 bytes. Maximum payload: 1184 bytes (1200 − 16).**
## 3.2 Message Types
| Code | Name | Direction | Purpose |
|:---|:---|:---|:---|
| `0x01` | `PING` | Request | Liveness probe; carries observed address (STUN response payload) |
| `0x02` | `PONG` | Response | Liveness ack; carries observed address |
| `0x03` | `FIND_NODE` | Request | DHT node lookup |
| `0x04` | `NODES` | Response | Return K closest nodes |
| `0x05` | `CRDT_DELTA` | Push | Single CRDT mutation (gossip) |
| `0x06` | `CRDT_DELTA_ACK` | Response | Acknowledgment of `CRDT_DELTA` |
| `0x07` | `AE_BEGIN` | Request | Initiate anti-entropy exchange |
| `0x08` | `AE_STATE` | Push | Full MV-Register state for one key |
| `0x09` | `AE_END` | Signal | Terminate anti-entropy batch |
| `0x0A` | `PING_REQ` | Request | Indirect liveness probe (SWIM) |
| `0x0B` | `PING_REQ_ACK` | Response | Indirect probe result |
| `0x0C` | `LEAVE` | Broadcast | Graceful departure announcement |
| `0x0D` | `PUNCH_REQ` | Request | Request third-party hole-punch coordination |
| `0x0E` | `PUNCH_SYN` | Push | Synchronized punching packet (both sides) |
| `0x0F` | `PUNCH_KEEPALIVE` | Push | NAT pinhole maintenance |
| `0x10` | `STREAM_OPEN` | Request | Open point-to-point stream |
| `0x11` | `STREAM_DATA` | Push | Stream payload chunk |
| `0x12` | `STREAM_CLOSE` | Signal | Close stream |
| `0x13` | `STREAM_ACK` | Response | Stream flow control acknowledgment |
| `0x14` | `RELAY_OFFER` | Request | Offer to relay a stream |
| `0x20` | `VOL_REPL_REQ` | Request | Request stateful volume snapshot |
| `0x21` | `VOL_REPL_CHUNK` | Push | Snapshot chunk (out-of-band stream type) |
| `0x22` | `VOL_REPL_DONE` | Signal | Snapshot transfer complete |
| `0x30` | `GW_CLAIM` | Push | Claim ownership of a Virtual IP (informational) |
## 3.3 Serialization Rules
1. **Byte order:** All multi-byte integers are big-endian.
2. **Alignment:** No alignment assumptions. Receivers extract fields via `memcpy`, then convert with `be16toh`/`be64toh`.
3. **No padding:** Payloads are packed; no implicit padding.
4. **Validation order:** (a) datagram length ≥ 16, (b) `version == 0x01`, (c) `msg_type` known, (d) `payload_len ≤ (datagram length − 16)`, (e) type-specific validation. Any failure drops the datagram silently.
5. **No dynamic allocation:** All parsing writes into pre-allocated stack or arena buffers.
## 3.4 Shared Structures
**NodeContact** (28 bytes):
```
0 cell_id 8 bytes (uint64)
8 addr 16 bytes (IPv6 or IPv4-mapped-IPv6)
24 port 2 bytes (uint16)
26 reach_class 1 byte (0=UNKNOWN, 1=PUBLIC, 2=PUNCHED, 3=RELAYED)
27 flags 1 byte (bit 0: BGP-speaker, bit 1: gateway-capable)
```
**VVEntry** (16 bytes):
```
0 actor_id 8 bytes
8 seq 8 bytes
```
**ObservedAddr** (18 bytes) — used in PING/PONG payloads for STUN:
```
0 addr 16 bytes
16 port 2 bytes
```
## 3.5 Message Payloads
### 3.5.1 PING (0x01) / PONG (0x02)
**Payload:** 18 bytes — `ObservedAddr` of the *peer* as observed by the sender. PONG echoes the PING `msg_nonce`.
The observed address is consumed by the receiver to update its own reachability class (Section 6.2).
### 3.5.2 FIND_NODE (0x03)
```
0 target_id 8 bytes
```
### 3.5.3 NODES (0x04)
```
0 count 2 bytes
2 NodeContact[count]
```
Max contacts per message: $\lfloor 1184 / 28 \rfloor = 42$.
### 3.5.4 CRDT_DELTA (0x05)
```
0 key_hash 8 bytes
8 dot_actor 8 bytes
16 dot_seq 8 bytes
24 ctx_count 1 byte
25 value_len 2 bytes
27 key_name_len 1 byte
28 context ctx_count × 16 bytes (VVEntry[])
... value value_len bytes
... key_name key_name_len bytes
```
**Semantic constraint:** A `CRDT_DELTA` contains exactly one entry. This is guaranteed because only fresh writes generate deltas (Section 7.4).
### 3.5.5 CRDT_DELTA_ACK (0x06)
```
0 key_hash 8 bytes
```
The header `msg_nonce` correlates the ACK to the original delta.
### 3.5.6 AE_BEGIN (0x07)
```
0 key_count 4 bytes
4 merkle_root 8 bytes (truncated SipHash of sorted state digest)
12 gvv_count 1 byte
13 global_vv gvv_count × 16 bytes
```
Carries a Merkle digest (Section 8.7) and the sender's global version vector (Section 8.6).
### 3.5.7 AE_STATE (0x08)
Carries the complete MV-Register for one key:
```
0 key_hash 8 bytes
8 entry_count 1 byte
9 ctx_count 1 byte
10 key_name_len 1 byte
11 entries entry_count × (8 + 8 + 2 + value_len)
... context ctx_count × 16 bytes
... key_name key_name_len bytes
```
### 3.5.8 AE_END (0x09)
**Empty payload.**
### 3.5.9 PING_REQ (0x0A) / PING_REQ_ACK (0x0B)
```
PING_REQ: target NodeContact (28 bytes)
PING_REQ_ACK: target_id (8) + reachable (1) + observed (18)
```
### 3.5.10 LEAVE (0x0C)
**Empty payload.**
### 3.5.11 PUNCH_REQ (0x0D)
Asks the receiver (a publicly reachable third party) to coordinate hole-punching between sender and a target:
```
0 target NodeContact 28 bytes
4 rendezvous_nonce 8 bytes (echoed in both sides' PUNCH_SYN)
```
### 3.5.12 PUNCH_SYN (0x0E)
```
0 rendezvous_nonce 8 bytes
```
Sent simultaneously by both peers at the time instructed by the coordinator. Carries no semantic payload beyond the nonce; its purpose is to open NAT pinholes in both directions.
### 3.5.13 PUNCH_KEEPALIVE (0x0F)
**Empty payload.** Sent to a `PUNCHED` peer at `PUNCH_KEEPALIVE_INTERVAL` to maintain the NAT mapping.
### 3.5.14 STREAM_OPEN (0x10)
```
0 stream_id 4 bytes
4 target_id 8 bytes
12 stream_type 1 byte (0x01=logs, 0x02=exec, 0x03=metrics, 0x04=volrepl)
13 key_name_len 1 byte
14 key_name key_name_len bytes
```
### 3.5.15 STREAM_DATA (0x11)
```
0 stream_id 4 bytes
4 seq_num 4 bytes
8 payload_len 2 bytes
10 payload payload_len bytes
```
Max payload per chunk: 1174 bytes.
### 3.5.16 STREAM_CLOSE (0x12)
```
0 stream_id 4 bytes
```
### 3.5.17 STREAM_ACK (0x13)
```
0 stream_id 4 bytes
4 ack_seq 4 bytes
```
### 3.5.18 RELAY_OFFER (0x14)
Sent by a cell offering to act as a relay for a stream that cannot be hole-punched:
```
0 stream_id 4 bytes
4 source_id 8 bytes
12 target_id 8 bytes
```
### 3.5.19 VOL_REPL_REQ (0x20)
```
0 volume_key_hash 8 bytes
8 base_generation 8 bytes (0 for full snapshot, else incremental)
```
### 3.5.20 VOL_REPL_CHUNK (0x21) / VOL_REPL_DONE (0x22)
Volume replication uses the same flow-control framing as `STREAM_DATA`/`STREAM_ACK`/`STREAM_CLOSE`. `VOL_REPL_CHUNK` and `STREAM_DATA` are structurally identical when `stream_type = 0x04`; the dedicated message codes exist for relay-node accounting (these streams may be large and are tracked separately).
### 3.5.21 GW_CLAIM (0x30)
```
0 vip 16 bytes (IPv6 or IPv4-mapped)
16 claim_priority 2 bytes (lower = stronger)
```
Informational announcement that the sender is currently advertising `vip` to upstream routers. Used for distributed gateway awareness (Section 14.4); the authoritative claim is the CRDT `gateway.<vip>` key.
---
# 4. Routing Table & Contact Records
*Implements: ROUTE-1..5, AVAIL-5.*
## 4.1 K-Bucket Structure
Each node maintains a routing table of at most 64 K-buckets, indexed by the position of the most-significant differing bit between the local Cell ID and a contact's Cell ID.
Each bucket holds at most $k$ entries:
| Node Type | Default $k$ | Minimum $k$ |
|:---|:---|:---|
| Standard (datacenter) | 20 | 4 |
| Constrained (edge/IoT) | 8 | 4 |
## 4.2 Contact Record
```c
struct Contact {
uint64_t cell_id;
struct sockaddr_in6 addr; // direct address from FIND_NODE/PING
struct sockaddr_in6 observed_addr; // STUN-discovered public address
uint64_t last_seen_ns;
uint64_t rtt_ewma_us;
uint16_t success_count;
uint16_t failure_count;
uint8_t reach_class; // PUBLIC | PUNCHED | RELAYED | UNKNOWN
uint8_t flags; // BGP-speaker, gateway-capable
uint64_t punched_at_ns; // last successful hole-punch
};
```
## 4.3 Insertion Policy
When a new contact is discovered:
1. If the bucket has fewer than $k$ entries, insert.
2. If full, compare new entry against the worst existing entry: rank by (reach_class better) > (failure_count lower) > (rtt_ewma_us lower).
3. Entries with recent successful exchanges are not evicted regardless of discovery order.
## 4.4 Contact Record Lifecycle
- Every received message refreshes `last_seen_ns`.
- Every PONG with an `ObservedAddr` updates `observed_addr` and possibly `reach_class`.
- Successful hole-punch updates `reach_class = PUNCHED` and `punched_at_ns`.
- $\ge$ `FAILURE_THRESHOLD` consecutive failures mark the contact stale; it may be evicted.
## 4.5 Iterative Lookup
```
PROCEDURE iterative_lookup(target_id):
candidates ← k local nodes closest to target_id by XOR
queried ← {}
REPEAT:
pick α unqueried nodes from closest_k, prioritized by reach_class then RTT
send FIND_NODE(target_id) in parallel
merge NODES responses into closest_k; keep k closest
add responders to queried
UNTIL no closer node observed in last round OR all closest_k queried
RETURN closest_k
```
Parameters: $k = 20$, $\alpha = 3$.
## 4.6 RTT Annealing
EWMA: $\text{rtt} = \alpha \cdot \text{sample} + (1 - \alpha) \cdot \text{prev}$, with $\alpha = 0.25$. RTT influences eviction, gossip peer selection, and lookup parallelism. It never influences bucket assignment.
## 4.7 Network Size Estimation
$$\hat{N} \approx k \cdot 2^{64-i}$$
where $i$ is the index of the nearest non-empty bucket. Used for display only.
---
# 5. Transport & I/O Architecture
*Implements: RES-1, RES-2, AVAIL-4, ARCH-5, ARCH-6.*
## 5.1 UDP Transport
All inter-node communication uses UDP.
- **Port:** 12421 default, configurable.
- **MTU:** Every datagram ≤ 1200 bytes (RES-2).
- **No TCP.** TCP's head-of-line blocking and connection-state overhead are unacceptable for one-off messages.
## 5.2 UDS Interface
Intra-node communication (CLI ↔ daemon) uses `SOCK_STREAM` Unix Domain Socket at `$COLLOID_DIR/colloid.sock`.
Framing: 1-byte command + 3-byte length + payload.
| Code | Command | Notes |
|:---|:---|:---|
| `0x01` | `SET` | Inject CRDT write |
| `0x02` | `GET` | Read MV-Register |
| `0x03` | `DELETE` | Tombstone write |
| `0x04` | `LIST` | Prefix scan |
| `0x05` | `STATUS` | Cluster summary |
| `0x06` | `RESOLVE` | Resolve `CONFLICT` |
| `0x07` | `EXEC` | Start exec stream |
| `0x08` | `LOGS` | Stream stdout/stderr |
| `0x09` | `WASM_INVOKE` | Synchronous Wasm RPC over UDS |
| `0x0A` | `VOL_LIST` | Enumerate local volumes |
Response framing: 1-byte status + 3-byte length + payload. Status: `OK`, `ERROR`, `NOT_FOUND`, `CONFLICT`.
## 5.3 io_uring Reactor
The daemon runs a **single-threaded, non-blocking event loop** backed by a single `io_uring` instance.
**Initialization:**
- `io_uring_setup(SQ_DEPTH=4096, IORING_SETUP_SQPOLL | IORING_SETUP_COOP_TASKRUN | IORING_SETUP_SINGLE_ISSUER)`.
- Submission queue (SQ) and completion queue (CQ) rings are mmap'd into user space.
- Buffer pool registered via `IORING_OP_PROVIDE_BUFFERS` with `BUFFER_POOL_SIZE` (default 1024) entries of 1200 bytes each. Inbound datagrams are written directly into pool buffers selected by the kernel.
- File descriptors are registered via `IORING_REGISTER_FILES_UPDATE`, eliminating fd table lookups per submission.
**Operations submitted:**
| Op | Purpose |
|:---|:---|
| `IORING_OP_RECVMSG_MULTISHOT` | Continuous UDP receive on the mesh socket; consumes registered buffers |
| `IORING_OP_SENDMSG_ZC` | Zero-copy UDP send for state and stream datagrams |
| `IORING_OP_ACCEPT_MULTISHOT` | UDS listener |
| `IORING_OP_READ` / `IORING_OP_WRITE` | UDS client sockets (fixed-buffer where possible) |
| `IORING_OP_TIMEOUT` / `IORING_OP_TIMEOUT_UPDATE` | Software timers (replaces a separate `timerfd`) |
| `IORING_OP_READ` on eventfd | Worker thread → reactor notifications |
| `IORING_OP_OPENAT`, `IORING_OP_READ`, `IORING_OP_FSYNC` | Volume persistence I/O (Section 10) — submitted from worker via shared SQ when applicable; otherwise from a dedicated worker `io_uring` |
**Submission discipline:**
- Reactor batches submissions; one `io_uring_enter` (or zero, with `SQPOLL`) per loop iteration.
- The reactor never blocks. On an empty completion queue, it issues `io_uring_enter(min_complete=1)` and yields to the kernel.
**RES-1 enforcement:** All submission buffers are pre-registered. Completion handling parses in place from registered receive buffers, runs CRDT merge into the arena, and returns the buffer to the pool via `IORING_OP_PROVIDE_BUFFERS` (or implicit re-arm).
## 5.4 Timer Management
Software timers are programmed as `IORING_OP_TIMEOUT` submissions tagged with a 64-bit user_data identifying the callback. The reactor maintains a min-heap of scheduled callbacks; when a TIMEOUT completion fires, the heap is consulted and the appropriate callback dispatched.
Scheduled timers:
| Timer | Interval | Purpose |
|:---|:---|:---|
| Anti-entropy | 30s | Random peer state exchange |
| Probe | 1s | SWIM direct probe |
| Heartbeat | 5s | Local heartbeat increment + gossip |
| Punch keepalive | 20s | NAT pinhole maintenance |
| Retry | exponential | Unacked CRDT_DELTA retransmit |
| eBPF stats reap | 1s | Aggregate per-VIP counters from kernel maps |
| Gateway re-affirm | 10s | Re-emit gratuitous ARP / BGP keepalive |
## 5.5 Worker Threads
The reactor offloads blocking or CPU-intensive work to worker threads via SPSC ring buffers:
- **Actuator worker:** OCI image pull/unpack, process spawn, container lifecycle.
- **Wasm worker:** Wasm compilation (one-shot, on workload SET) and short-RPC execution.
- **Volume worker:** snapshot, snapshot-receive, replication chunk hashing.
- **eBPF worker:** program load, map updates outside the hot path, perf-buffer drain.
Workers signal the reactor via `eventfd` reads driven by `IORING_OP_READ`. No worker holds a CRDT lock; CRDT mutations are produced by submitting messages back to the reactor.
## 5.6 Mock Transport
For deterministic testing, the reactor's `io_uring` interface is wrapped behind a thin abstraction layer. In the test harness, submissions are intercepted and routed to an in-memory delivery queue with configurable loss, duplication, and reorder policies. CRDT correctness tests run against this harness.
---
# 6. NAT Traversal & Reachability
*Implements: AVAIL-5, BOOT-4, ROUTE-5, STREAM-6.*
## 6.1 Reachability Model
Each cell publishes its `reach_class` in routing table responses and in the `health.<id>.contact` CRDT key:
| Class | Definition | Direct send possible? |
|:---|:---|:---|
| `PUBLIC` | Observed public address equals bound socket address; reachable from any peer. | Yes |
| `PUNCHED` | Behind cone-like NAT. Reachable after a hole has been punched. | Yes, after PUNCH_SYN exchange |
| `RELAYED` | Behind symmetric NAT or restrictive firewall. Direct UDP impossible. | No; uses relay |
| `UNKNOWN` | Not yet probed. | Treated as `RELAYED` until classified |
## 6.2 STUN-Style Address Discovery
Every PING carries an `ObservedAddr` payload containing the *receiver's* address as seen by the *sender*. When the daemon receives a PONG (or any other message containing an `ObservedAddr`), it:
1. Records the observed `(addr, port)` mapping.
2. If the observed mapping equals the locally bound socket address → `PUBLIC`.
3. If the observed mapping differs but is consistent across at least 3 peers → `PUNCHED`.
4. If the observed mapping varies per peer (varying ports across peers) → `RELAYED`.
Reclassification is performed at most every `REACH_REPROBE_INTERVAL` (60s) to avoid CRDT churn.
## 6.3 Hole Punching
When cell A wants to communicate with cell B, both behind NAT:
1. A consults its routing table for B. If B's `reach_class == PUBLIC`, send directly.
2. If B is `PUNCHED`, send to B's `observed_addr`. If recent `punched_at_ns` is fresh (< `PUNCH_TTL`), the pinhole is open.
3. If B is `PUNCHED` but pinhole is stale, or B is `RELAYED` and A is `PUBLIC`, A initiates punch:
a. A finds a coordinator C: a `PUBLIC` cell present in both A's and B's routing tables (selected from A's K-bucket entries by lowest RTT). If none exists, fall back to relay (Section 11.4).
b. A sends `PUNCH_REQ(target=B, nonce=R)` to C.
c. C forwards `PUNCH_REQ(target=A, nonce=R)` to B.
d. Both A and B, upon receiving the coordination message, simultaneously send `PUNCH_SYN(R)` to each other's `observed_addr`. The first packets are typically dropped by the other side's NAT; the *outbound* packets create pinholes.
e. Subsequent packets traverse the pinholes. Both sides update `reach_class = PUNCHED` and `punched_at_ns` on receipt of any post-PUNCH_SYN datagram.
4. If both A and B are `RELAYED` (symmetric NAT on both ends), hole punching is impossible. A uses a `RELAYED` path through C (Section 11.4).
## 6.4 Pinhole Maintenance
For every `PUNCHED` peer with `now - last_sent_ns >= PUNCH_KEEPALIVE_INTERVAL`, the daemon sends a `PUNCH_KEEPALIVE`. Keepalive frequency is set conservatively (20s default) to outlive UDP mapping timeouts on common consumer NATs (≈30s).
Keepalives are the **only** state-plane traffic that violates AVAIL-4 in a quiescent cluster. They are bounded: $O(\text{PUNCHED contacts})$ per cell per `PUNCH_KEEPALIVE_INTERVAL`.
## 6.5 Bootstrap with NAT
`colloid connect <seed>`:
1. Generate Cell ID.
2. Send `PING` to seed. The seed's PONG carries the joiner's observed address.
3. Joiner classifies itself per Section 6.2 (initial classification uses just the seed; refined as more peers are contacted).
4. Joiner records its observed address. This is the address it will advertise.
5. Joiner performs iterative lookup for its own ID; encountered peers learn the joiner's `observed_addr` from PONGs they receive back.
6. Standard bootstrap continues (anti-entropy, ACTIVE).
If the seed itself is behind NAT, only `RELAYED` joins are possible until a `PUBLIC` cell is discovered through the seed.
## 6.6 Public Rendezvous
A cluster intended to span NAT boundaries must contain at least one `PUBLIC` cell. The CLI command `colloid connect <hostname>` accepts DNS-resolvable hostnames; operators are expected to expose at least one such cell via a stable DNS name (`seed.colloid.example.com`).
This is the only deployment constraint; it does not introduce a SPOF because once a NAT'd cell has joined and discovered other `PUBLIC` cells, it does not depend on the original seed.
---
# 7. State Model (CRDTs)
*Implements: SAFE-1..6, AVAIL-1, AVAIL-2, CONV-2, CRDT-ACTOR, RES-1, RES-3.*
## 7.1 MV-Register Structure
The global state is a map from **key hash** (uint64) to **MV-Register**.
```c
struct MVRegister {
uint64_t key_hash;
char key_name[MAX_KEY_NAME];
uint8_t key_name_len;
uint8_t entry_count;
struct {
uint64_t dot_actor;
uint64_t dot_seq;
uint16_t value_len;
uint8_t value[MAX_VALUE_SIZE];
} entries[MAX_ENTRIES];
uint8_t ctx_count;
struct {
uint64_t actor_id;
uint64_t seq;
} context[MAX_CTX_ENTRIES];
};
```
Compile-time limits:
| Constant | Value | Rationale |
|:---|:---|:---|
| `MAX_KEY_NAME` | 128 | Longest reasonable key path |
| `MAX_VALUE_SIZE` | 512 | Sufficient for WorkloadSpec + volume metadata |
| `MAX_ENTRIES` | 4 | Conflicts beyond 3-way are operationally improbable |
| `MAX_CTX_ENTRIES` | 32 | Upper bound on $W_K$ |
| `MAX_KEYS` | 16384 | Arena pre-allocation |
## 7.2 Version Vectors
A **version vector** is a map from Cell IDs to sequence numbers, representing observed causal history for a key. A **dot** is `(actor_id, seq)` identifying a single write.
Dominance: $\text{dom}(d, V) \iff V[d.\text{actor}] \ge d.\text{seq}$.
Per CRDT-ACTOR, actors are daemon Cell IDs. Per-key vectors grow with $W_K$, typically 1–5 entries (16–80 bytes).
## 7.3 State-Based Merge (Anti-Entropy)
Given two complete states $S_A, S_B$ for the same key:
$$E_A' = \{(v, d) \in S_A \mid \neg\text{dom}(d, S_B.\text{ctx}) \lor (v, d) \in S_B\}$$
$$E_B' = \{(v, d) \in S_B \mid \neg\text{dom}(d, S_A.\text{ctx}) \lor (v, d) \in S_A\}$$
$$\text{merge}(S_A, S_B).\text{entries} = E_A' \cup E_B'$$
$$\text{merge}(S_A, S_B).\text{ctx} = \text{pointwise\_max}(S_A.\text{ctx}, S_B.\text{ctx})$$
Merge is commutative, associative, idempotent → SEC.
## 7.4 Delta Merge (Gossip)
A delta is a `CRDT_DELTA` carrying exactly one entry and the writer's full context for the key. It is structurally a single-entry state and merges by the formula in 7.3.
**Local write at daemon X for key K with value V:**
```
PROCEDURE write(K, V):
X.seq ← X.seq + 1
dot ← (X.cell_id, X.seq)
K.entries ← [(V, dot)] // replaces all entries
K.ctx[X.cell_id] ← X.seq // update context
delta ← CRDT_DELTA(K.key_hash, dot, K.ctx, V, K.key_name)
gossip(delta) // Section 8.1
```
The write supersedes everything X has observed; if K was conflicted, the write resolves the conflict because the new context dominates all existing dots.
## 7.5 Conflict Detection
K is in `CONFLICT` iff $|K.\text{entries}| > 1$. Checked locally after every merge. The actuator skips conflicted configuration keys (SAFE-1).
## 7.6 Key Categories and Conflict Policy
| Category | Key pattern | Conflict policy | Rationale |
|:---|:---|:---|:---|
| Configuration | `spread.*`, `stateful.*`, `vip.*` | **Block** (SAFE-1) | Silent resolution unsafe |
| Health status | `health.<id>.status` | **Auto-resolve to ALIVE** | Empirical proof of liveness |
| Heartbeat | `health.<id>.heartbeat` | **Never conflicts** ($W_K = 1$) | Single writer |
| Contact | `health.<id>.contact` | **Latest by dot.seq from owner** | Owner-only writes |
| Volume metadata | `volume.<id>.meta` | **Block** (SAFE-1) | Divergent volume state is unsafe |
| Gateway claim | `gateway.<vip>` | **Block** (SAFE-1) | Concurrent claims must surface |
| String table | `_str.<hash>` | **Auto-resolve to longest** | Cosmetic |
The health auto-resolution is a read-time policy applied by the actuator, not a CRDT merge rule. All values are preserved on disk per SAFE-4.
## 7.7 Key Hashing
`key_hash = SipHash-2-4(k0, k1, key_name_bytes)`. $(k_0, k_1)$ is a fixed 128-bit constant; configurable for namespace isolation in the enterprise extension. The same SipHash key is used for Rendezvous hashing (Section 11.1).
## 7.8 Arena Allocation
```c
static struct MVRegister crdt_arena[MAX_KEYS];
static size_t crdt_count;
```
Hash table with open addressing maps `key_hash → arena_index`. Exhaustion of `MAX_KEYS` rejects insertion and logs an error; the operator must resize and restart.
---
# 8. Anti-Entropy & Protocol Reliability
*Implements: CONV-2, AVAIL-2, AVAIL-4.*
## 8.1 Gossip
When a `CRDT_DELTA` is produced (local write or received from a peer), forward to `GOSSIP_FANOUT = 3` peers selected as:
1. The K-bucket nearest to `key_hash XOR local_cell_id`.
2. Within that bucket, prefer `reach_class = PUBLIC` over `PUNCHED` over `RELAYED`, then lowest RTT.
3. If fewer than `GOSSIP_FANOUT` candidates, spill to adjacent buckets.
Forwarding terminates via the dedup ring (Section 8.4).
## 8.2 Anti-Entropy Exchange
Every `AE_INTERVAL` (30s):
1. Select a random K-bucket entry $B$.
2. Send `AE_BEGIN(key_count, merkle_root, global_vv)`.
3. If $B$'s `merkle_root` matches local, skip state exchange.
4. Otherwise, perform Merkle drill-down (Section 8.7) and send `AE_STATE` for differing keys only.
5. Send `AE_END`.
6. $B$ responds symmetrically.
Rate-limit: `AE_RATE_LIMIT` (100 msg/s per exchange).
## 8.3 ACK and Retry
Each `CRDT_DELTA` is enqueued in a retry ring as `(msg_nonce, target, attempt, next_retry_ns)`. On matching `CRDT_DELTA_ACK`, the entry is removed. On timeout `RETRY_BASE × 2^attempt`, retransmit and increment. After `RETRY_MAX = 3` attempts, abandon; anti-entropy is the backstop.
The retry ring is a pre-allocated fixed-size structure; eviction policy: drop oldest untried.
## 8.4 Deduplication
Each node maintains a fixed-size dedup ring of fingerprints:
$$\text{fp} = \text{key\_hash} \oplus \text{dot\_actor} \oplus (\text{dot\_seq} \times \text{0x9E3779B97F4A7C15})$$
Ring size: 4096 entries, linear probing, oldest overwritten on collision. Re-merge of an evicted-and-resent delta is harmless (idempotent).
## 8.5 Tombstones
A delete is a write with `value_len = 0`. The MV-Register handles this as a regular write; the actuator treats empty values as "no workload."
Tombstones cannot be freed until proven received globally (Section 8.6).
## 8.6 Watermark Calculation
Each node maintains a **global version vector**:
$$G_X[a] = \max_{K} K.\text{ctx}[a]$$
Global VVs are piggybacked on `AE_BEGIN`. The local watermark is:
$$W[a] = \min_{X \in \text{known}} G_X[a]$$
A tombstone with dot $(a, s)$ is GC'd when $W[a] \ge s$. Stale anti-entropy participation conservatively keeps the watermark low.
## 8.7 Merkle Anti-Entropy
The state arena is augmented with a Merkle tree over the sorted key-hash space. Tree depth: 16 (covers up to $2^{16} = 65536$ leaves, where each leaf may bucket multiple keys for clusters > 16K keys).
Each leaf digest is `SipHash(sorted key_hashes ∥ each MV-Register dot_set digest)`. Internal nodes hash their children.
On `AE_BEGIN`, the sender includes `merkle_root` (64 bits, truncated). The receiver:
1. Compares roots; if equal, skip (no traffic).
2. If different, sends `AE_MERKLE_REQ(depth, prefix_bits)` (a sub-message type embedded in a follow-up `AE_STATE`-like frame) to identify divergent subtrees.
3. Sends `AE_STATE` for keys within divergent leaves only.
The Merkle tree is updated incrementally on every CRDT write (the leaves containing the touched key are re-hashed; the path to the root is updated; total cost $O(\log \text{leaves})$ per write).
This reduces anti-entropy bandwidth from $O(|S|)$ to $O(\Delta)$ where $\Delta$ is divergent key count.
---
# 9. Failure Detection & Health
*Implements: EXEC-3, AVAIL-3.*
## 9.1 SWIM Probing
Every `PROBE_INTERVAL` (1s): select a random K-bucket entry as the probe target. Send `PING`. Wait `PROBE_TIMEOUT` (500ms) for `PONG`.
## 9.2 Indirect Probing
On direct probe failure, send `PING_REQ(target)` to `INDIRECT_PROBE_COUNT = 3` random K-bucket peers. Each peer attempts its own `PING` and returns `PING_REQ_ACK(reachable, observed_addr)`.
If any positive ACK arrives within `PROBE_TIMEOUT`, the target is alive. Otherwise, the target enters local `SUSPECTED` state with a `SUSPECT_TIMEOUT` (5s) timer.
## 9.3 Death Declaration
If `SUSPECT_TIMEOUT` expires with no successful contact:
1. Detecting node writes `health.<target_id>.status = DEAD`.
2. Mutation gossips; all nodes learn of the death.
3. All nodes:
a. Remove the dead cell from the healthy set $H$.
b. Recompute placement (Section 11).
c. If the dead cell held any VIP, the new VIP owner per CRDT initiates ARP/BGP takeover (Section 14).
4. The dead cell's contact remains in routing tables but is marked stale; it may be evicted.
## 9.4 Resurrection
A restarted node, on observing `health.<self>.status = DEAD`, immediately writes `ALIVE`. Any node receiving a valid UDP message from a `DEAD`-marked cell writes `ALIVE` on its behalf.
## 9.5 Health Auto-Resolution
If `health.<id>.status` is conflicted with at least one `ALIVE` entry, the read-time policy treats the cell as alive. SAFE-4 still holds: all entries persist in the MV-Register.
## 9.6 Resource Hysteresis
Resource metrics (`health.<id>.cpu`, `.mem`, `.disk`) are gossiped only when:
$$\left|\frac{\text{new} - \text{last\_gossiped}}{\text{last\_gossiped}}\right| \ge 0.15$$
This suppresses minor fluctuations while propagating meaningful changes.
## 9.7 eBPF-Assisted Liveness
A loaded eBPF program (Section 12.3) increments per-peer ingress counters on every received datagram from a recognized Cell ID. The daemon reads these counters at probe time; if a counter has incremented since the last probe, the peer is implicitly alive and a synthetic PONG is treated as received. This reduces probe traffic in steady-state clusters and validates ARCH-5.
---
# 10. Stateful Workloads & Volumes
*Implements: SAFE-5, EXEC-5.*
## 10.1 Volume Model
A **volume** is a named directory tree owned by a workload. The volume lifecycle is tied to a `stateful` workload but is conceptually independent in the CRDT.
Volumes are identified by `volume_id = SipHash(workload_key_name ∥ replica_index)`. Volume metadata is published in the CRDT key `volume.<volume_id>.meta`:
```
struct VolumeMeta {
uint64_t volume_id;
uint64_t owner_cell; // current anchor cell
uint8_t replica_index; // 0..R-1
uint64_t generation; // monotonically increasing per snapshot
uint64_t content_hash; // SipHash of canonical snapshot at generation
uint64_t size_bytes;
uint8_t replication_factor; // RF, including the owner
uint64_t replicas[7]; // peer cell IDs holding replicas (RF ≤ 8)
};
```
Volume contents are **not** stored in the CRDT (would violate ARCH-1 and explode state size). Contents live on local disk at `$COLLOID_DIR/volumes/<volume_id>/`.
## 10.2 Storage Layout
```
$COLLOID_DIR/volumes/<volume_id>/
├── meta (mirrors VolumeMeta, fsync'd on write)
├── current/ (live filesystem)
└── snapshots/<generation>/ (read-only snapshots for replication)
```
Snapshots use the host filesystem's native mechanism (overlayfs lower layer + `reflink` copy on supporting filesystems, or `mount --bind` + `cp --reflink=auto`). On filesystems without reflink, a full copy is performed by the volume worker.
Volume metadata writes are `fsync`'d (departing from NON-6 specifically for volumes, since volume contents are externally persisted).
## 10.3 Stateful Placement
A workload declared `stateful` carries `replication_factor = RF` and `replicas = R`. Placement proceeds as:
1. Compute the candidate placement for each replica index $i \in [0, R)$ using Rendezvous hashing on the tuple `(workload_key_hash, replica_index=i)`.
2. The cell with the highest score becomes the **owner** for replica $i$.
3. The next $RF - 1$ highest-scoring cells become **followers** holding additional copies of the volume.
The owner runs the workload; followers hold the data but do not execute. Followers exist to allow rapid takeover on owner failure.
## 10.4 Volume Replication
When a stateful workload is created:
1. Owner cell creates an empty local volume.
2. Owner publishes `volume.<volume_id>.meta` with `generation = 0`.
3. Follower cells observe the meta key (via CRDT gossip).
4. Each follower opens a `STREAM_OPEN` with `stream_type = 0x04 (volrepl)` to the owner.
5. Owner sends the empty initial state via `VOL_REPL_CHUNK`/`VOL_REPL_DONE`.
On each volume mutation:
1. Owner snapshots the current state into `snapshots/<generation+1>/`.
2. Owner writes `volume.<volume_id>.meta` with `generation = generation + 1` and updated `content_hash`.
3. Followers detect the generation bump, issue `VOL_REPL_REQ(volume_id, base_generation=current_local_gen)` to the owner.
4. Owner sends a delta (file additions, modifications, deletions between the two generations) via stream messages.
5. Followers apply the delta atomically (write to a staging tree, swap via rename, fsync).
Snapshots older than $G$ generations behind any known replica are GC'd (default $G = 4$).
## 10.5 Anchored Placement Adjustment
Because volumes are expensive to migrate, Rendezvous scores for stateful workloads are biased toward the current owner:
$$\text{adjusted\_score}(W, C) = \begin{cases} \text{score}(W, C) + \text{ANCHOR\_BIAS} & \text{if } C = \text{current owner in CRDT} \\ \text{score}(W, C) & \text{otherwise}\end{cases}$$
`ANCHOR_BIAS` is set to a value larger than any plausible Rendezvous score difference (in practice, `0x4000_0000_0000_0000`). This guarantees the owner does not change unless the owner is removed from $H$.
## 10.6 Owner Failover
When the owner is declared `DEAD`:
1. The Rendezvous calculation, with the owner removed from $H$, selects a new owner — by construction the highest-scoring follower (because followers were the next-RF-1 cells in the original ranking).
2. The new owner verifies it holds a recent local copy (within `MAX_FAILOVER_LAG_GENERATIONS = 2`).
3. If yes, it writes `volume.<volume_id>.meta` with itself as `owner_cell`, advancing the dot for that key. This claim acts as the takeover declaration.
4. The actuator on the new owner reconciles: the stateful workload appears in desired state on the new owner, which starts the workload pointed at the local volume.
5. New followers are selected and begin replication from the new owner.
If no follower has a sufficiently recent copy, the meta key may go to `CONFLICT` (if two cells concurrently attempt to claim ownership during a partition). The operator resolves via `colloid resolve`.
## 10.7 Partition Behavior
During a partition that splits owner from followers:
- The owner's partition continues writing to the volume; generation advances.
- Followers in the other partition see no updates.
- If the failure detector in the follower partition declares the owner dead, a new owner is elected from the followers. **Both sides now have divergent volume state.**
- On heal, both `volume.<volume_id>.meta` entries surface as `CONFLICT` (different owners, different generations). SAFE-1 holds: the actuator stops writing to the volume on all cells.
- The operator inspects both branches and resolves manually (`colloid resolve volume.<id>.meta`). One branch's data is lost; this is the inescapable consequence of AP partition tolerance for stateful workloads (NON-2).
## 10.8 Wasm Workload State
Wasm workloads with `runtime_class = wasm` declare which volumes they mount (if any). State access from Wasm is limited to:
- WASI filesystem APIs scoped to the mounted volume directories.
- WASI clock and random APIs (deterministic when configured per SAFE-6).
- Colloid-specific host functions (Section 13.5).
No raw filesystem or network access; capabilities are explicit.
---
# 11. Scheduling (Rendezvous Hashing)
*Implements: EXEC-1, EXEC-2, EXEC-3, EXEC-4, EXEC-5.*
## 11.1 Hash Function
$$\text{score}(W, C, i) = \text{SipHash}_{k_0, k_1}(W.\text{key\_hash}\ \|\ C.\text{cell\_id}\ \|\ i)$$
where $i$ is the replica index. The same SipHash key as Section 7.7.
## 11.2 Healthy Set
$$H = \{\ C\ :\ \text{health.}C\text{.status auto-resolves to ALIVE or is absent}\ \}$$
## 11.3 Placement
```
PROCEDURE compute_placement(W, R, H):
if W.runtime_class == wasm:
H' ← {C ∈ H : C.flags.wasm_capable}
else:
H' ← H
placement ← []
for i in 0..R-1:
scores ← {C : adjusted_score(W, C, i) for C in H'}
if W.stateful:
owner ← argmax_C (scores[C] biased per Section 10.5)
else:
owner ← argmax_C scores[C]
placement.append(owner)
return placement
```
For non-stateful workloads, picking R independent argmaxes per replica index may select the same cell multiple times in tiny clusters; the actuator runs at most one instance per cell unless the workload explicitly opts in to oversubscription.
## 11.4 Failover
When a cell is removed from $H$:
1. Every surviving node recomputes placement.
2. Workloads whose top replica was the dead cell get a new owner (the next-highest scorer).
3. The new owner's actuator detects "desired but not running" and starts the workload (after volume re-replication completes, for stateful).
4. Workloads unaffected by the failure see no placement change (EXEC-3).
## 11.5 Replica Semantics
Workload types supported in M1:
- `spread` — run $R$ replicas across the cluster.
- `stateful` — run $R$ replicas with anchored volumes.
A workload key encodes its type via the key prefix (`spread.*` or `stateful.*`).
---
# 12. Kernel Data Plane (eBPF / XDP)
*Implements: ARCH-5, RES-5, NON-7 boundary.*
## 12.1 Program Set
Colloid loads the following eBPF programs at daemon startup. Each is compiled into the binary as bytecode and loaded via `bpf(2)` `BPF_PROG_LOAD`.
| Program | Type | Attach Point | Purpose |
|:---|:---|:---|:---|
| `colloid_vip_xdp` | `BPF_PROG_TYPE_XDP` | Egress interface (host) | VIP DNAT: rewrite destination IP/port for service VIPs to a selected backend pod IP |
| `colloid_vip_tc_egress` | `BPF_PROG_TYPE_SCHED_CLS` | `clsact` egress | SNAT return path: rewrite source back to VIP for outbound replies |
| `colloid_sock_lookup` | `BPF_PROG_TYPE_SK_LOOKUP` | per-netns | Steer connections matching VIP+port to the local daemon's socket if backend is local |
| `colloid_peer_counter` | `BPF_PROG_TYPE_XDP` | Mesh interface | Per-peer ingress packet/byte counters for liveness assist (Section 9.7) |
| `colloid_arp_gw` | `BPF_PROG_TYPE_XDP` | Gateway interface | Intercept gratuitous ARP and ARP requests for VIPs we own (Section 14) |
| `colloid_observe_tc` | `BPF_PROG_TYPE_SCHED_CLS` | `clsact` ingress | L4 flow counters per workload (metrics surface) |
## 12.2 Map Set
| Map | Type | Key | Value | Purpose |
|:---|:---|:---|:---|:---|
| `vip_backends` | `BPF_MAP_TYPE_HASH` | `(vip, port)` | array of `(backend_ip, backend_port, weight)` | VIP load distribution |
| `vip_consistent_hash` | `BPF_MAP_TYPE_ARRAY` | hash bucket | backend index | Maglev-style consistent hash for L4 stickiness |
| `peer_counters` | `BPF_MAP_TYPE_PERCPU_HASH` | `cell_id` | `(pkts, bytes, last_ns)` | Liveness assist |
| `vip_owned` | `BPF_MAP_TYPE_HASH` | `vip` | `(owner_cell, claim_priority)` | Local view of VIP ownership |
| `flow_table` | `BPF_MAP_TYPE_LRU_HASH` | 5-tuple | backend index | Connection tracking |
| `workload_stats` | `BPF_MAP_TYPE_PERCPU_HASH` | `workload_id` | flow stats | Per-workload observability |
Map sizes are computed at daemon startup from cluster scale parameters (`MAX_VIPS`, `MAX_BACKENDS_PER_VIP`, `MAX_PEERS`) and never resized at runtime, satisfying RES-5.
## 12.3 Service VIP Data Path
When a workload declares a VIP (in `WorkloadSpec.vip` field), the daemon:
1. Writes the CRDT key `vip.<vip>` containing the VIP definition (backends, port, mode).
2. On observing the key, every cell updates its local `vip_backends` map with all backend endpoints (cell-local backends marked with a "local" flag).
3. The cell that holds gateway responsibility (Section 14) advertises the VIP via gratuitous ARP / BGP.
4. Incoming packets to the VIP hit `colloid_vip_xdp` at line rate:
- 5-tuple hash → consistent hash bucket → backend selection.
- DNAT rewrite (destination IP/port → backend pod IP/port).
- `bpf_redirect` or kernel forwarding to backend.
5. Return packets hit `colloid_vip_tc_egress`, which SNATs the backend address back to the VIP.
The consistent hash map is rebuilt by the daemon (user space, via the eBPF worker thread) whenever backends change, then swapped atomically by replacing the map content. Existing flows in `flow_table` are preserved during the swap to avoid breaking established connections.
## 12.4 Connection Tracking
The `flow_table` LRU map stores `5-tuple → backend_index` for the lifetime of a connection. Entries auto-expire by LRU pressure (default 65536 entries). For long-lived connections, the entry is refreshed on each packet.
When a backend is removed from `vip_backends` (workload stopped or cell died), existing flows continue routing to the (now-stale) backend index until LRU eviction or explicit invalidation; new flows are routed to the new backend set. This trades brief disruption for sustained connection stability — acceptable for the AP model.
## 12.5 Observability
`colloid_observe_tc` populates `workload_stats` with packet/byte counters keyed by destination workload. The daemon reads this map every `STATS_REAP_INTERVAL` (1s) and:
- Aggregates into local rolling windows.
- Surfaces in `colloid status --metrics` and over the stream protocol with `stream_type = 0x03 (metrics)`.
- Optionally publishes summary CRDT keys (`metric.<workload>.rate`) at coarse intervals.
## 12.6 Program Verification & Compatibility
All programs are compiled with `clang -target bpf` and embedded as a section in the daemon binary. On startup, each program is loaded with `BPF_PROG_LOAD` and verified by the kernel verifier; if any program fails to load (older kernel without required features), the daemon logs a warning and falls back to a user-space implementation of the same function (slower, but functionally identical for the cluster).
Minimum kernel: 5.15 (for `BPF_PROG_TYPE_SK_LOOKUP`, `IORING_OP_SENDMSG_ZC`, `IORING_SETUP_SINGLE_ISSUER`).
## 12.7 Map Updates from User Space
Map updates are submitted via `BPF_MAP_UPDATE_ELEM` ioctls from the eBPF worker thread. The reactor thread never blocks on map updates: it enqueues an `UPDATE_VIP` or `UPDATE_PEER` command on the eBPF worker's ring, and the worker executes the syscall.
For reads (e.g., per-peer counters), the worker periodically polls maps and pushes aggregated results back to the reactor via eventfd.
---
# 13. Actuator & Runtime Backends
*Implements: EXEC-1, EXEC-4, SAFE-6.*
## 13.1 Architecture
The actuator dispatches workloads by `runtime_class`:
```
.───────────────. Ring Buffer .─────────────────.
| Reactor |─── WorkloadCommand[] ───>| Actuator Worker |
| (epoll | | (dispatches by |
| via io_uring)|<── eventfd notify ──────| runtime_class) |
'───────────────' '────────┬────────┘
│
┌───────────────────┼──────────────────┐
▼ ▼ ▼
.────────────────. .────────────────. .──────────────.
| OCI Runtime | | Wasm Runtime | | Process |
| (namespaces, | | (in-process | | (direct |
| cgroups, OCI) | | Wasm engine) | | exec) |
'────────────────' '────────────────' '──────────────'
```
## 13.2 WorkloadSpec
A workload specification stored in the CRDT:
```
0 runtime_class 1 byte (0x01=oci, 0x02=wasm, 0x03=process)
1 flags 1 byte (bit 0: stateful, bit 1: requires_vip)
2 replicas 2 bytes
4 cpu_millicores 2 bytes
6 mem_mb 2 bytes
8 image_len 1 byte
9 image UTF-8 (OCI ref or Wasm module URL or binary path)
... port_count 1 byte
... ports port_count × 4 bytes (host:container)
... vip_count 1 byte
... vips vip_count × 18 bytes (vip + port)
... capability_mask 4 bytes (Wasm host capability bitfield)
... volume_count 1 byte
... volumes volume_count × (16 bytes: id + mount_len + mount_path)
```
Maximum size: 512 bytes (fits in `MAX_VALUE_SIZE`).
## 13.3 OCI Backend
For `runtime_class = oci`, the actuator worker:
1. **Image pull:** HTTP/1.1 client against the OCI registry (Docker Hub, GHCR, etc.). OAuth2 bearer token retrieval. Manifest + layer blobs downloaded to `$COLLOID_DIR/images/`.
2. **Unpack:** Layers extracted with overlayfs `lowerdir` chain. `mount(2)` with `overlay` type.
3. **Namespace setup:** `clone(2)` with `CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWNS | CLONE_NEWNET | CLONE_NEWUSER`.
4. **Filesystem:** `pivot_root(2)` into the overlay mount.
5. **Cgroups v2:** Write CPU and memory limits to `/sys/fs/cgroup/colloid/<workload_id>/`.
6. **Networking:** `netlink(7)` creates veth pair (`vethX` on host, `eth0` in container); bridge to `colloid0`; assign pod IP from per-cell pool; NAT via nftables.
7. **Exec:** Runs the entrypoint from the OCI config.
Image storage is content-addressed by layer digest; common layers are deduplicated across workloads.
## 13.4 Process Backend
For `runtime_class = process`, the actuator executes a host-native binary directly under a cgroup with the declared CPU/memory limits. No filesystem or network isolation. Intended for system-level workloads (sidecars, observability agents) that need full host access.
## 13.5 Wasm Backend
For `runtime_class = wasm`, the actuator worker invokes the embedded Wasm runtime. The runtime is statically linked into the daemon (no external `wasmtime`/`wasmer` dependency); the engine implements a subset of:
- Wasm 1.0 core spec.
- WASI Preview 1 (clocks, env, fd_* for mounted volumes, args, random_get).
- Colloid host functions (see below).
**Lifecycle:**
1. On workload SET, the worker downloads the Wasm module (HTTP) and compiles it to native code once. Compiled artifacts are cached in `$COLLOID_DIR/wasm-cache/<sha256>.so`.
2. For each replica, the worker instantiates the compiled module with:
- WASI environment from `WorkloadSpec.env`.
- Mounted volumes as WASI preopens.
- Host capabilities per `capability_mask`.
3. The module runs in a dedicated thread with cooperative scheduling. CPU and memory limits enforced by cgroup membership of the Wasm worker thread.
**Host capability bitfield:**
| Bit | Capability | Description |
|:---|:---|:---|
| 0 | `KV_READ` | Read CRDT keys matching workload-scoped prefix |
| 1 | `KV_WRITE` | Write CRDT keys (mutations performed by daemon, attributed to daemon Cell ID) |
| 2 | `STREAM` | Open streams to other workloads |
| 3 | `NET_OUTBOUND` | Outbound TCP/UDP via host network namespace |
| 4 | `VOL_RW` | Read/write declared volumes |
| 5 | `TIMER` | Schedule callback invocations |
| 6 | `METRICS` | Emit per-invocation metrics |
Capabilities not granted return WASI `errno = EPERM`.
**Wasm host ABI (selected functions):**
```
colloid_kv_get(key_ptr, key_len, out_buf, out_buf_len) → out_len or -errno
colloid_kv_set(key_ptr, key_len, val_ptr, val_len) → 0 or -errno
colloid_stream_open(target_workload_ptr, target_workload_len, stream_type) → stream_fd or -errno
colloid_metric_emit(name_ptr, name_len, value: f64) → 0 or -errno
```
**Determinism:** When `WorkloadSpec.flags.deterministic` is set, the runtime forbids access to `random_get`, system clock (substituted with a logical clock incremented per-invocation), and capability bits incompatible with determinism. This supports SAFE-6.
**Wasm RPC invocation:** `colloid wasm invoke <name> <args>` issues a synchronous `WASM_INVOKE` command over UDS. The daemon dispatches to the Wasm worker, which executes the module's exported `colloid_invoke` function and returns the result. Useful for short, deterministic compute.
## 13.6 Reconciliation
The reactor runs reconciliation whenever:
- A CRDT merge changes any `spread.*` or `stateful.*` key.
- A health key changes (alters $H$).
- A volume meta key changes.
- The worker reports a workload exit.
```
PROCEDURE reconcile():
desired ← {}
for each K matching "spread.*" or "stateful.*":
if K is CONFLICT: skip
if K.value is empty: skip
W ← deserialize WorkloadSpec from K.value
for i in 0..W.replicas-1:
placement ← compute_placement(W, i, H)
if placement == my_cell_id:
if W.stateful and volume not yet replicated locally:
enqueue VOL_REPLICATE command; skip
desired.add((K.key_hash, i))
for each (k, i) in desired but not in running:
enqueue START command with appropriate runtime backend
for each (k, i) in running but not in desired:
enqueue STOP command
```
---
# 14. Gateway Continuity (ARP / BGP)
*Implements: AVAIL-6, CONV-3.*
## 14.1 Gateway Problem
When a service VIP is associated with the cluster, external traffic destined for that VIP must arrive at *some* cell hosting (or able to forward to) the backend workloads. If that cell fails, traffic must redirect to a surviving cell within bounded time and without external load balancer involvement.
Colloid provides two mechanisms, used singly or in combination based on network environment:
- **L2 mode (gratuitous ARP):** for VIPs in the same broadcast domain as cluster cells.
- **L3 mode (BGP):** for VIPs announced to upstream routers via BGP (eBGP to a top-of-rack router, or BGP unnumbered).
## 14.2 VIP Ownership Election
For each VIP defined in `vip.<vip>` CRDT key, ownership is computed deterministically:
$$\text{owner}(\text{vip}) = \underset{C \in H_{\text{gw}}}{\arg\max}\ \text{SipHash}(\text{vip}\ \|\ C.\text{cell\_id})$$
where $H_{\text{gw}} \subseteq H$ is the set of gateway-capable cells (flag bit set in their contact record). If $H_{\text{gw}}$ is empty, the VIP is unowned and traffic to it is dropped — operator misconfiguration.
The owner is computed locally by every cell (EXEC-2). Only the elected owner advertises the VIP externally.
## 14.3 L2 Mode (Gratuitous ARP)
When a cell becomes VIP owner:
1. The daemon writes `gateway.<vip> = (my_cell_id, claim_priority, generation)`.
2. The daemon sends a `GW_CLAIM` informational message to all K-bucket peers (for fast convergence; the CRDT is the source of truth).
3. The daemon constructs and emits a gratuitous ARP response on the configured gateway interface:
- `Sender Hardware Address`: cell's MAC on the gateway interface.
- `Sender Protocol Address`: VIP.
- `Target Protocol Address`: VIP (broadcast announcement).
4. The gratuitous ARP is repeated 3 times at 100ms intervals (RFC 5227 style) to overcome packet loss.
5. The `colloid_arp_gw` eBPF program at the gateway interface intercepts ARP requests for owned VIPs and responds with the cell's MAC.
6. The cell binds the VIP as a secondary IP on the gateway interface (so the kernel accepts inbound traffic).
When the previous owner is declared dead:
1. The new owner (computed deterministically) writes `gateway.<vip>` with the new owner_cell.
2. Upon observing the CRDT key change locally, the new owner performs steps 3–6 above. Switches and other hosts on the segment learn the new MAC ↔ VIP binding from the gratuitous ARP.
3. The previous owner, if still alive (false positive death declaration), observes the CRDT key change, releases the VIP binding, and stops responding to ARP for it. The conflict (if any) surfaces as a `CONFLICT` on `gateway.<vip>` (per Section 7.6, gateway claims block).
If `gateway.<vip>` is in `CONFLICT`, all cells release the VIP binding (SAFE-1). External traffic stops until the operator resolves. This is preferable to flapping or split-brain advertisement.
## 14.4 L3 Mode (BGP)
For VIPs that must be announced beyond the L2 segment, gateway-capable cells maintain an eBGP session with an upstream router. The daemon embeds a minimal BGP speaker:
- BGP-4 (RFC 4271) speaker, advertise-only (does not consume routes from the peer for cluster routing).
- TCP/179 session to the configured peer router.
- Capability negotiation: IPv4 + IPv6 unicast; no multiprotocol extensions beyond these.
- Hold time: 30s default.
**Operation:**
1. On daemon startup, if the cell has BGP configuration (`bgp.peer_address`, `bgp.peer_asn`, `bgp.local_asn` in local config or CRDT), establish the BGP session.
2. On becoming VIP owner (per Section 14.2), advertise the VIP as a /32 (or /128 for IPv6) route via `UPDATE` messages.
3. On ceasing to be owner, send a withdrawal `UPDATE`.
4. The next-hop attribute is the cell's own interface address on the BGP session peer subnet.
BGP convergence is bounded by the upstream router's BGP timers and routing table propagation; typically 1–5 seconds on a single-hop eBGP session.
## 14.5 Mode Selection
Each VIP in `vip.<vip>` declares its mode (`L2`, `L3`, or `BOTH`). The owner cell:
- For `L2`: performs gratuitous ARP (Section 14.3) only.
- For `L3`: performs BGP advertisement (Section 14.4) only.
- For `BOTH`: performs both. ARP handles same-segment hosts; BGP handles routed traffic.
## 14.6 Convergence Sequence
Failure → restoration timeline:
| t | Event |
|:---|:---|
| 0 | VIP owner cell crashes |
| 0 → $T_{\text{SWIM}}$ | Failure detection (Section 9): probe → indirect probe → suspect → death declaration |
| $T_{\text{SWIM}}$ | `health.<dead>.status = DEAD` written and gossiped |
| $T_{\text{SWIM}} + T_{\text{gossip}}$ | All cells observe the death; new owner computed locally |
| $T_{\text{SWIM}} + T_{\text{gossip}} + \epsilon$ | New owner writes `gateway.<vip>` and begins ARP/BGP takeover |
| ... + $T_{\text{ARP}}$ | L2 hosts updated (typically < 1s) |
| ... + $T_{\text{BGP}}$ | Upstream router converges (typically 1–5s) |
Total: typically 6–10s for end-to-end gateway recovery. This is CONV-3.
## 14.7 Pre-emption Protection
To avoid VIP flapping under marginal network conditions, a new owner waits `GW_TAKEOVER_DEBOUNCE` (2s) after writing `gateway.<vip>` before emitting ARP/BGP. If during the debounce the CRDT key changes again (e.g., the previous owner self-corrected), the new owner aborts the takeover.
## 14.8 Anti-Spoofing Discipline
The ARP/BGP mechanisms here are *cooperative* among Colloid cells — they are not adversarial spoofing of unrelated hosts. The `colloid_arp_gw` eBPF program responds to ARP requests **only** for VIPs whose ownership the local CRDT confirms. Cells never respond for IPs they do not own, and never on interfaces not configured as gateway interfaces.
In hostile network environments where ARP-based VIP migration is forbidden by policy, operators should configure L3-only mode.
---
# 15. Configuration & CLI
*Implements: ARCH-2, BOOT-1, BOOT-3, CRDT-ACTOR.*
## 15.1 Pipeline
```
.───────────────. .───────────────. .──────────────.
| Config file |─────| CLI process |─UDS─| Local daemon |─UDP─> mesh
| (human text) | | (parse, ser.) | | (CRDT write) |
'───────────────' '───────────────' '──────────────'
```
The CLI validates input, serializes to binary `WorkloadSpec`, and issues `SET` over UDS. The daemon performs the CRDT write using its own Cell ID as actor (CRDT-ACTOR).
## 15.2 Config Format (Illustrative)
```
spread nginx {
image docker.io/nginx:latest
replicas 3
port 80:8080
port 443:8443
vip 10.0.0.100:80 mode=both
cpu 500m
mem 256mb
}
stateful redis {
image docker.io/redis:7
replicas 1
replication_factor 3
port 6379:6379
volume data /data
cpu 1000m
mem 1024mb
}
wasm transformer {
module https://wasm.example.com/transform.wasm
replicas 5
capabilities kv_read,metrics
deterministic true
}
```
Each block becomes a CRDT key: `spread.nginx`, `stateful.redis`, `wasm.transformer`.
## 15.3 Conflict Resolution Workflow
```
$ colloid status
CONFLICT on spread.nginx:
Value (replicas=5, image=nginx:1.25) by Cell 0x1A2B at {0x1A2B:3, 0x3C4D:1}
Value (replicas=2, image=nginx:1.24) by Cell 0x5E6F at {0x5E6F:1, 0x3C4D:1}
$ colloid resolve spread.nginx --from-file nginx.colloid
OK
```
## 15.4 Volume Inspection
```
$ colloid volumes
VOLUME OWNER GEN SIZE REPLICAS
redis-0 cell-0x1A 42 1.2GB [0x3C, 0x5E]
postgres-0 cell-0x3C 118 8.7GB [0x1A, 0x5E, 0x7F]
```
## 15.5 Installation
```
curl -sfL https://colloid.net/install.sh | sh
```
Single statically linked binary. eBPF bytecode embedded.
---
# 16. Stream Protocol
*Implements: STREAM-1..6.*
## 16.1 Lifecycle
```
STREAM_OPEN → [STREAM_DATA / STREAM_ACK]* → STREAM_CLOSE
```
Stream IDs are 32-bit, source-generated. `(source_cell_id, stream_id)` is globally unique.
## 16.2 Target Resolution
Local: read CRDT, compute placement, identify target cell. Zero network traffic.
## 16.3 Path Selection
For target cell $T$:
1. If $T$ is in local routing table with `reach_class = PUBLIC` → direct send.
2. If $T$ is `PUNCHED` and pinhole is fresh → direct send.
3. If $T$ is `PUNCHED` but pinhole stale, or $T$ is `RELAYED`, attempt hole-punch (Section 6.3). If success, direct send.
4. If hole-punch fails or both ends symmetric → relay (Section 16.4).
## 16.4 Relay
When direct send is impossible, the source uses DHT-routed forwarding: `STREAM_OPEN` is sent to the K-closest peer to $T$ that the source can reach. That peer either forwards to $T$ directly (if $T$ is reachable from it) or forwards to its own K-closest peer to $T$. At each hop, an entry is added to the relay table:
```c
struct StreamRelay {
uint32_t stream_id;
uint64_t source_id;
struct sockaddr_in6 inbound_peer;
struct sockaddr_in6 outbound_peer;
uint64_t created_ns;
uint64_t last_activity_ns;
};
```
`MAX_STREAM_RELAYS = 256` per node, pre-allocated.
## 16.5 Flow Control
Sliding window of `STREAM_WINDOW = 8` unacknowledged chunks. Receiver sends `STREAM_ACK(ack_seq)` for highest contiguous. Timeout `STREAM_TIMEOUT = 2s`, retry up to `STREAM_RETRY_MAX = 5`.
## 16.6 Relay Cleanup
Relay entries idle for `RELAY_IDLE_TIMEOUT = 30s` are GC'd.
---
# 17. Milestones & Implementation Plan
## 17.1 Milestone 1: Mesh Foundation
| Task | Deliverable |
|:---|:---|
| 1. io_uring reactor | Single-threaded loop, registered buffers, multishot recvmsg, timeout submission |
| 2. Kademlia overlay | XOR distance, K-buckets with reach-class awareness, iterative lookup |
| 3. Wire protocol | All M1 message types, validation state machine |
| 4. CRDT engine | MV-Register, version vectors, state and delta merge, conflict detection |
| 5. Gossip & anti-entropy | Epidemic forwarding, dedup, Merkle anti-entropy |
| 6. SWIM failure detection | Direct/indirect probing, suspicion, death CRDT writes |
| 7. STUN + hole punching | Address discovery, three-party punch coordination, keepalives |
| 8. Rendezvous scheduling | Placement, healthy set derivation, reconciliation |
| 9. OCI runtime backend | clone(), pivot_root, cgroups v2, netlink, OCI registry client |
| 10. CLI | UDS, config parser, conflict resolution workflow |
**External dependencies: none** (Wasm engine and BGP speaker embedded as later milestones; OCI handled natively in M1).
## 17.2 Milestone 2: Kernel Data Plane
| Task | Deliverable |
|:---|:---|
| 1. eBPF program set | XDP VIP DNAT, TC egress SNAT, sk_lookup, peer counters |
| 2. eBPF map management | User-space updates, atomic swap for consistent hash |
| 3. Service VIP integration | CRDT `vip.*` keys drive map state; eBPF intercepts traffic |
| 4. Gratuitous ARP for VIPs | `colloid_arp_gw` program + user-space ARP emit |
| 5. eBPF-assisted liveness | `colloid_peer_counter` integration into SWIM (Section 9.7) |
| 6. Observability | Per-workload stats via `colloid_observe_tc` |
| 7. User-space fallback | If eBPF load fails, equivalent user-space DNAT path |
## 17.3 Milestone 3: Stateful & Multi-Runtime
| Task | Deliverable |
|:---|:---|
| 1. Volume engine | Local volume storage, snapshots (reflink + fallback), generation tracking |
| 2. Volume replication | `VOL_REPL_*` messages, delta computation, atomic apply |
| 3. Stateful placement | Anchored Rendezvous, owner failover, conflict on divergent meta |
| 4. Embedded Wasm runtime | Wasm 1.0 core + WASI Preview 1 + Colloid host functions |
| 5. Wasm capability gating | Capability bitfield enforcement, deterministic mode |
| 6. Process runtime backend | Cgroup-only execution for host-native workloads |
## 17.4 Milestone 4: L3 Gateway
| Task | Deliverable |
|:---|:---|
| 1. Embedded BGP speaker | BGP-4 advertise-only, TCP/179 session, IPv4+IPv6 unicast |
| 2. VIP announcement | UPDATE generation on ownership change, withdrawal on departure |
| 3. Mode selection | L2/L3/BOTH per VIP, debounce, conflict handling |
| 4. Convergence testing | Failover timing instrumentation, CONV-3 validation |
---
# Appendix A: Protocol Constants
| Constant | Default | Section |
|:---|:---|:---|
| `PROTOCOL_VERSION` | 1 | 3.1 |
| `DEFAULT_PORT` | 12421 | 5.1 |
| `MAX_DATAGRAM` | 1200 | 5.1 |
| `HEADER_SIZE` | 16 | 3.1 |
| `PAYLOAD_MAX` | 1184 | 3.1 |
| `K_BUCKET_SIZE` | 20 | 4.1 |
| `K_BUCKET_MIN` | 4 | 4.1 |
| `ALPHA` | 3 | 4.5 |
| `GOSSIP_FANOUT` | 3 | 8.1 |
| `AE_INTERVAL` | 30s | 8.2 |
| `AE_RATE_LIMIT` | 100 msg/s | 8.2 |
| `RETRY_BASE` | 200ms | 8.3 |
| `RETRY_MAX` | 3 | 8.3 |
| `DEDUP_RING_SIZE` | 4096 | 8.4 |
| `PROBE_INTERVAL` | 1s | 9.1 |
| `PROBE_TIMEOUT` | 500ms | 9.1 |
| `INDIRECT_PROBE_COUNT` | 3 | 9.2 |
| `SUSPECT_TIMEOUT` | 5s | 9.2 |
| `HYSTERESIS_THRESHOLD` | 0.15 | 9.6 |
| `HEARTBEAT_INTERVAL` | 5s | 5.4 |
| `REFRESH_INTERVAL` | 15min | 4.4 |
| `FAILURE_THRESHOLD` | 5 consecutive | 4.4 |
| `RTT_EWMA_ALPHA` | 0.25 | 4.6 |
| `PUNCH_KEEPALIVE_INTERVAL` | 20s | 6.4 |
| `PUNCH_TTL` | 60s | 6.3 |
| `REACH_REPROBE_INTERVAL` | 60s | 6.2 |
| `STREAM_WINDOW` | 8 | 16.5 |
| `STREAM_TIMEOUT` | 2s | 16.5 |
| `STREAM_RETRY_MAX` | 5 | 16.5 |
| `MAX_STREAM_RELAYS` | 256 | 16.4 |
| `RELAY_IDLE_TIMEOUT` | 30s | 16.6 |
| `MAX_KEYS` | 16384 | 7.1 |
| `MAX_KEY_NAME` | 128 | 7.1 |
| `MAX_VALUE_SIZE` | 512 | 7.1 |
| `MAX_ENTRIES` | 4 | 7.1 |
| `MAX_CTX_ENTRIES` | 32 | 7.1 |
| `MAX_VIPS` | 1024 | 12.2 |
| `MAX_BACKENDS_PER_VIP` | 256 | 12.2 |
| `MAX_PEERS` | 4096 | 12.2 |
| `STATS_REAP_INTERVAL` | 1s | 12.5 |
| `ANCHOR_BIAS` | 0x4000_0000_0000_0000 | 10.5 |
| `MAX_FAILOVER_LAG_GENERATIONS` | 2 | 10.6 |
| `GW_TAKEOVER_DEBOUNCE` | 2s | 14.7 |
| `SQ_DEPTH` | 4096 | 5.3 |
| `BUFFER_POOL_SIZE` | 1024 | 5.3 |
| `MIN_KERNEL` | 5.15 | 12.6 |
# Appendix B: Invariant Traceability
| Invariant | Implementing Sections |
|:---|:---|
| SAFE-1 | 7.5, 7.6, 13.6 |
| SAFE-2 | 7.5 |
| SAFE-3 | 7.4, 15.3 |
| SAFE-4 | 7.3 |
| SAFE-5 | 10.3, 10.5, 11.3 |
| SAFE-6 | 13.5 |
| AVAIL-1 | 7.3 |
| AVAIL-2 | 8.2 |
| AVAIL-3 | 4, 9, 11 |
| AVAIL-4 | 5.3, 5.4 |
| AVAIL-5 | 6 |
| AVAIL-6 | 14 |
| CONV-1 | 4 |
| CONV-2 | 8.1, 8.2 |
| CONV-3 | 14.6 |
| ARCH-1 | 7, 16 |
| ARCH-2 | 15.1 |
| ARCH-3 | 8.1 |
| ARCH-4 | 7.8, 15 |
| ARCH-5 | 12 |
| ARCH-6 | 5.3 |
| BOOT-1 | 6.5 |
| BOOT-2 | 6.5, 6.6 |
| BOOT-3 | 2.3, 15 |
| BOOT-4 | 6.2, 6.5 |
| EXEC-1 | 11.3, 13.6 |
| EXEC-2 | 11.3 |
| EXEC-3 | 11.4 |
| EXEC-4 | 13.2, 13.3, 13.4, 13.5 |
| EXEC-5 | 10.5, 10.6 |
| ROUTE-1 | 4.1 |
| ROUTE-2 | 4.5 |
| ROUTE-3 | 4.1 |
| ROUTE-4 | 4.3, 4.6 |
| ROUTE-5 | 4.2, 6.1 |
| STREAM-1..6 | 16, 6.3 |
| RES-1 | 5.3, 7.8 |
| RES-2 | 3.1, 5.1 |
| RES-3 | 7.8, 8.2 |
| RES-4 | 2.1, 2.5 |
| RES-5 | 12.2 |
| CRDT-ACTOR | 7.4, 5.2 |
|