Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: deterministic topo sort using iterative dfs #471

Merged
merged 5 commits into from
Feb 18, 2025
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
55 changes: 22 additions & 33 deletions packages/object/src/hashgraph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,48 +163,37 @@ export class HashGraph {
this.arePredecessorsFresh = false;
}

kahnsAlgorithm(origin: Hash, subgraph: ObjectSet<Hash>): Hash[] {
dfsTopologicalSortIterative(origin: Hash, subgraph: ObjectSet<Hash>): Hash[] {
const visited = new ObjectSet<Hash>();
const result: Hash[] = [];
const inDegree = new Map<Hash, number>();
const queue: Hash[] = [];
const stack: Hash[] = [origin];
const processing = new ObjectSet<Hash>();

for (const hash of subgraph.entries()) {
inDegree.set(hash, 0);
}
while (stack.length > 0) {
const node = stack[stack.length - 1];

for (const [vertex, children] of this.forwardEdges) {
if (!inDegree.has(vertex)) continue;
for (const child of children) {
if (!inDegree.has(child)) continue;
inDegree.set(child, (inDegree.get(child) || 0) + 1);
if (visited.has(node)) {
stack.pop();
result.push(node);
processing.delete(node);
continue;
}
}

let head = 0;
queue.push(origin);
while (queue.length > 0) {
const current = queue[head];
head++;
if (!current) continue;

result.push(current);
processing.add(node);
visited.add(node);

for (const child of this.forwardEdges.get(current) || []) {
if (!inDegree.has(child)) continue;
const inDegreeValue = inDegree.get(child) || 0;
inDegree.set(child, inDegreeValue - 1);
if (inDegreeValue - 1 === 0) {
queue.push(child);
const neighbors = this.forwardEdges.get(node);
if (neighbors) {
for (const neighbor of neighbors.sort()) {
if (processing.has(neighbor)) throw new Error("Graph contains a cycle!");
if (subgraph.has(neighbor) && !visited.has(neighbor)) {
stack.push(neighbor);
}
}
}

if (head > queue.length / 2) {
queue.splice(0, head);
head = 0;
}
}

return result;
return result.reverse();
}

/* Topologically sort the vertices in the whole hashgraph or the past of a given vertex. */
Expand All @@ -213,7 +202,7 @@ export class HashGraph {
origin: Hash = HashGraph.rootHash,
subgraph: ObjectSet<Hash> = new ObjectSet(this.vertices.keys())
): Hash[] {
const result = this.kahnsAlgorithm(origin, subgraph);
const result = this.dfsTopologicalSortIterative(origin, subgraph);
if (!updateBitsets) return result;
this.reachablePredecessors.clear();
this.topoSortedIndex.clear();
Expand Down
13 changes: 8 additions & 5 deletions packages/object/tests/actiontypes.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AddMulDRP } from "@ts-drp/blueprints/src/AddMul/index.js";
import { beforeAll, beforeEach, describe, expect, test } from "vitest";
import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest";

import { DRPObject, ObjectACL } from "../src/index.js";

Expand All @@ -26,6 +26,9 @@ describe("Test: ActionTypes (Nop and Swap)", () => {
drp2 = new DRPObject({ peerId: "peer2", drp: new AddMulDRP(), acl });
addMul = drp.drp as AddMulDRP;
addMul2 = drp2.drp as AddMulDRP;

vi.useFakeTimers();
vi.setSystemTime(new Date(Date.UTC(1998, 11, 19)));
});

test("Test: Nop", () => {
Expand Down Expand Up @@ -92,17 +95,17 @@ describe("Test: ActionTypes (Nop and Swap)", () => {
addMul2.add(5);
drp.merge(drp2.vertices);
drp2.merge(drp.vertices);
expect(addMul.query_value()).toBe(55);
expect(addMul2.query_value()).toBe(55);
expect(addMul.query_value()).toBe(75);
expect(addMul2.query_value()).toBe(75);

addMul2.mul(2);
addMul2.add(2);
addMul.add(3);
addMul.mul(3);
drp.merge(drp2.vertices);
drp2.merge(drp.vertices);
expect(addMul.query_value()).toBe(354);
expect(addMul2.query_value()).toBe(354);
expect(addMul.query_value()).toBe(480);
expect(addMul2.query_value()).toBe(480);
});
});

Expand Down
69 changes: 64 additions & 5 deletions packages/object/tests/hashgraph.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { MapConflictResolution, MapDRP } from "@ts-drp/blueprints/src/Map/index.js";
import { SetDRP } from "@ts-drp/blueprints/src/Set/index.js";
import { beforeAll, beforeEach, describe, expect, test } from "vitest";
import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest";

import { ObjectACL } from "../src/acl/index.js";
import { Vertex } from "../src/hashgraph/index.js";
import { ActionType, SemanticsType, Vertex } from "../src/hashgraph/index.js";
import {
ACLGroup,
DRP,
Expand Down Expand Up @@ -47,7 +47,10 @@ function selfCheckConstraints(hg: HashGraph): boolean {
}
}

const topoOrder = hg.kahnsAlgorithm(HashGraph.rootHash, new ObjectSet(hg.vertices.keys()));
const topoOrder = hg.dfsTopologicalSortIterative(
HashGraph.rootHash,
new ObjectSet(hg.vertices.keys())
);

for (const vertex of hg.getAllVertices()) {
if (!topoOrder.includes(vertex.hash)) {
Expand All @@ -67,6 +70,9 @@ describe("HashGraph construction tests", () => {
beforeEach(async () => {
obj1 = new DRPObject({ peerId: "peer1", acl, drp: new SetDRP<number>() });
obj2 = new DRPObject({ peerId: "peer2", acl, drp: new SetDRP<number>() });

vi.useFakeTimers();
vi.setSystemTime(new Date(Date.UTC(1998, 11, 19)));
});

test("Test: Vertices are consistent across data structures", () => {
Expand Down Expand Up @@ -105,11 +111,64 @@ describe("HashGraph construction tests", () => {

const linearOps = obj2.hashGraph.linearizeOperations();
expect(linearOps).toEqual([
{ opType: "add", value: [2], drpType: DrpType.DRP },
{ opType: "add", value: [1], drpType: DrpType.DRP },
{ opType: "add", value: [2], drpType: DrpType.DRP },
] as Operation[]);
});

test("Test: Should detect cycle in topological sort", () => {
const hashgraph = new HashGraph(
"",
(_vertices: Vertex[]) => {
return {
action: ActionType.Nop,
};
},
(_vertices: Vertex[]) => {
return {
action: ActionType.Nop,
};
},
SemanticsType.pair
);
const frontier = hashgraph.getFrontier();
const v1 = newVertex(
"",
{
opType: "test",
value: [1],
drpType: DrpType.DRP,
},
frontier,
Date.now(),
new Uint8Array()
);
hashgraph.addVertex(v1);

const v2 = newVertex(
"",
{
opType: "test",
value: [2],
drpType: DrpType.DRP,
},
[v1.hash],
Date.now(),
new Uint8Array()
);
hashgraph.addVertex(v2);

// create a cycle
hashgraph.forwardEdges.set(v2.hash, [HashGraph.rootHash]);

expect(() => {
hashgraph.dfsTopologicalSortIterative(
HashGraph.rootHash,
new ObjectSet(hashgraph.vertices.keys())
);
}).toThrowError("Graph contains a cycle!");
});

test("Test: HashGraph with 2 root vertices", () => {
/*
ROOT -- V1:ADD(1)
Expand Down Expand Up @@ -259,8 +318,8 @@ describe("HashGraph for AddWinSet tests", () => {
const linearOps = obj1.hashGraph.linearizeOperations();
const expectedOps: Operation[] = [
{ opType: "add", value: [1], drpType: DrpType.DRP },
{ opType: "delete", value: [1], drpType: DrpType.DRP },
{ opType: "add", value: [2], drpType: DrpType.DRP },
{ opType: "delete", value: [1], drpType: DrpType.DRP },
];
expect(linearOps).toEqual(expectedOps);
});
Expand Down
Loading
Loading