From 4c518e667efb41742453bc5f59c0523298f63116 Mon Sep 17 00:00:00 2001 From: winprn Date: Thu, 2 Jan 2025 11:53:02 +0700 Subject: [PATCH 01/10] feat: use Kahn's algorithm for finding toposort --- packages/object/src/hashgraph/index.ts | 43 ++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index bde9a13d..95aadbb1 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -256,14 +256,53 @@ export class HashGraph { return result; } + kahnAlgorithm(origin: Hash, subgraph: ObjectSet): Hash[] { + const result: Hash[] = []; + const inDegree = new Map(); + const queue: Hash[] = []; + + for (const hash of subgraph.entries()) { + inDegree.set(hash, 0); + } + + for (const [_, children] of this.forwardEdges) { + for (const child of children) { + inDegree.set(child, (inDegree.get(child) || 0) + 1); + } + } + + queue.push(origin); + while (queue.length > 0) { + const current = queue.shift(); + if (!current) continue; + + result.push(current); + + for (const child of this.forwardEdges.get(current) || []) { + if (inDegree.has(child)) { + const inDegreeValue = inDegree.get(child); + if (inDegreeValue === undefined) { + log.error("::hashgraph::Kahn: Undefined in-degree value"); + return []; + } + inDegree.set(child, inDegreeValue - 1); + if (inDegree.get(child) === 0) { + queue.push(child); + } + } + } + } + + return result; + } + /* Topologically sort the vertices in the whole hashgraph or the past of a given vertex. */ topologicalSort( updateBitsets = false, origin: Hash = HashGraph.rootHash, subgraph: ObjectSet = new ObjectSet(this.vertices.keys()), ): Hash[] { - const result = this.depthFirstSearch(origin, subgraph); - result.reverse(); + const result = this.kahnAlgorithm(origin, subgraph); if (!updateBitsets) return result; this.reachablePredecessors.clear(); this.topoSortedIndex.clear(); From 21721638a05dca3aa57462079f827294e43dd3bf Mon Sep 17 00:00:00 2001 From: winprn Date: Thu, 2 Jan 2025 13:02:29 +0700 Subject: [PATCH 02/10] fix: only consider vertices in current subgraph --- packages/object/src/hashgraph/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index 95aadbb1..30c2883e 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -279,6 +279,7 @@ export class HashGraph { result.push(current); for (const child of this.forwardEdges.get(current) || []) { + if (!subgraph.has(child)) continue; if (inDegree.has(child)) { const inDegreeValue = inDegree.get(child); if (inDegreeValue === undefined) { From 9e7190e72fede20cf834671c7ff9a36006017e04 Mon Sep 17 00:00:00 2001 From: winprn Date: Thu, 2 Jan 2025 13:02:41 +0700 Subject: [PATCH 03/10] fix: update test to reflect latest implementation --- packages/object/tests/hashgraph.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/object/tests/hashgraph.test.ts b/packages/object/tests/hashgraph.test.ts index 19706c74..7f3802fc 100644 --- a/packages/object/tests/hashgraph.test.ts +++ b/packages/object/tests/hashgraph.test.ts @@ -49,8 +49,8 @@ describe("HashGraph construction tests", () => { const linearOps = obj2.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ - { type: "add", value: 1 }, { type: "add", value: 2 }, + { type: "add", value: 1 }, ]); }); @@ -170,8 +170,8 @@ describe("HashGraph for AddWinSet tests", () => { const linearOps = obj1.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ { type: "add", value: 1 }, - { type: "add", value: 2 }, { type: "remove", value: 1 }, + { type: "add", value: 2 }, ]); }); @@ -204,8 +204,8 @@ describe("HashGraph for AddWinSet tests", () => { expect(linearOps).toEqual([ { type: "add", value: 1 }, { type: "add", value: 1 }, - { type: "remove", value: 5 }, { type: "add", value: 10 }, + { type: "remove", value: 5 }, ]); }); @@ -235,9 +235,9 @@ describe("HashGraph for AddWinSet tests", () => { const linearOps = obj1.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ - { type: "add", value: 1 }, { type: "add", value: 1 }, { type: "add", value: 2 }, + { type: "add", value: 1 }, ]); }); @@ -288,12 +288,12 @@ describe("HashGraph for AddWinSet tests", () => { const linearOps = obj1.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ + { type: "add", value: 1 }, { type: "add", value: 1 }, { type: "remove", value: 2 }, { type: "add", value: 2 }, - { type: "add", value: 1 }, - { type: "add", value: 3 }, { type: "remove", value: 1 }, + { type: "add", value: 3 }, ]); }); @@ -345,11 +345,11 @@ describe("HashGraph for AddWinSet tests", () => { const linearOps = obj1.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ { type: "add", value: 1 }, - { type: "remove", value: 2 }, { type: "add", value: 1 }, { type: "remove", value: 2 }, - { type: "add", value: 3 }, + { type: "remove", value: 2 }, { type: "remove", value: 1 }, + { type: "add", value: 3 }, { type: "add", value: 2 }, { type: "remove", value: 1 }, ]); @@ -602,7 +602,7 @@ describe("Vertex timestamp tests", () => { test("Test: Vertex's timestamp must not be less than any of its dependencies' timestamps", () => { /* __ V1:ADD(1) __ - / \ + / \ ROOT -- V2:ADD(2) ---- V4:ADD(4) (invalid) \ / -- V3:ADD(3) -- From 8a04a584b909d0451b45f54d0504e28fa74dd210 Mon Sep 17 00:00:00 2001 From: winprn Date: Thu, 2 Jan 2025 13:05:53 +0700 Subject: [PATCH 04/10] improv: make BFS faster --- packages/object/src/hashgraph/index.ts | 27 +++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index 30c2883e..184eb990 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -271,27 +271,32 @@ export class HashGraph { } } + let head = 0; queue.push(origin); while (queue.length > 0) { - const current = queue.shift(); + const current = queue[head]; + head++; if (!current) continue; result.push(current); for (const child of this.forwardEdges.get(current) || []) { if (!subgraph.has(child)) continue; - if (inDegree.has(child)) { - const inDegreeValue = inDegree.get(child); - if (inDegreeValue === undefined) { - log.error("::hashgraph::Kahn: Undefined in-degree value"); - return []; - } - inDegree.set(child, inDegreeValue - 1); - if (inDegree.get(child) === 0) { - queue.push(child); - } + const inDegreeValue = inDegree.get(child); + if (inDegreeValue === undefined) { + log.error("::hashgraph::Kahn: Undefined in-degree value"); + return []; + } + inDegree.set(child, inDegreeValue - 1); + if (inDegree.get(child) === 0) { + queue.push(child); } } + + if (head > queue.length / 2) { + queue.splice(0, head); + head = 0; + } } return result; From f825dffb412c418806a3acbd9960ced21133f390 Mon Sep 17 00:00:00 2001 From: winprn Date: Thu, 2 Jan 2025 14:24:37 +0700 Subject: [PATCH 05/10] Revert "fix: update test to reflect latest implementation" This reverts commit 9e7190e72fede20cf834671c7ff9a36006017e04. --- packages/object/tests/hashgraph.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/object/tests/hashgraph.test.ts b/packages/object/tests/hashgraph.test.ts index 7f3802fc..19706c74 100644 --- a/packages/object/tests/hashgraph.test.ts +++ b/packages/object/tests/hashgraph.test.ts @@ -49,8 +49,8 @@ describe("HashGraph construction tests", () => { const linearOps = obj2.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ - { type: "add", value: 2 }, { type: "add", value: 1 }, + { type: "add", value: 2 }, ]); }); @@ -170,8 +170,8 @@ describe("HashGraph for AddWinSet tests", () => { const linearOps = obj1.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ { type: "add", value: 1 }, - { type: "remove", value: 1 }, { type: "add", value: 2 }, + { type: "remove", value: 1 }, ]); }); @@ -204,8 +204,8 @@ describe("HashGraph for AddWinSet tests", () => { expect(linearOps).toEqual([ { type: "add", value: 1 }, { type: "add", value: 1 }, - { type: "add", value: 10 }, { type: "remove", value: 5 }, + { type: "add", value: 10 }, ]); }); @@ -236,8 +236,8 @@ describe("HashGraph for AddWinSet tests", () => { const linearOps = obj1.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ { type: "add", value: 1 }, - { type: "add", value: 2 }, { type: "add", value: 1 }, + { type: "add", value: 2 }, ]); }); @@ -288,12 +288,12 @@ describe("HashGraph for AddWinSet tests", () => { const linearOps = obj1.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ - { type: "add", value: 1 }, { type: "add", value: 1 }, { type: "remove", value: 2 }, { type: "add", value: 2 }, - { type: "remove", value: 1 }, + { type: "add", value: 1 }, { type: "add", value: 3 }, + { type: "remove", value: 1 }, ]); }); @@ -344,12 +344,12 @@ describe("HashGraph for AddWinSet tests", () => { const linearOps = obj1.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ - { type: "add", value: 1 }, { type: "add", value: 1 }, { type: "remove", value: 2 }, + { type: "add", value: 1 }, { type: "remove", value: 2 }, - { type: "remove", value: 1 }, { type: "add", value: 3 }, + { type: "remove", value: 1 }, { type: "add", value: 2 }, { type: "remove", value: 1 }, ]); @@ -602,7 +602,7 @@ describe("Vertex timestamp tests", () => { test("Test: Vertex's timestamp must not be less than any of its dependencies' timestamps", () => { /* __ V1:ADD(1) __ - / \ + / \ ROOT -- V2:ADD(2) ---- V4:ADD(4) (invalid) \ / -- V3:ADD(3) -- From 72fcded20868e7bc96943d800385d4e736740c54 Mon Sep 17 00:00:00 2001 From: winprn Date: Thu, 2 Jan 2025 14:27:28 +0700 Subject: [PATCH 06/10] apply suggestion --- packages/object/src/hashgraph/index.ts | 6 ++++-- packages/object/tests/hashgraph.test.ts | 18 +++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index 184eb990..e8348951 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -265,8 +265,10 @@ export class HashGraph { inDegree.set(hash, 0); } - for (const [_, children] of this.forwardEdges) { + for (const [vertex, children] of this.forwardEdges) { + if (!subgraph.has(vertex)) continue; for (const child of children) { + if (!subgraph.has(child)) continue; inDegree.set(child, (inDegree.get(child) || 0) + 1); } } @@ -281,7 +283,7 @@ export class HashGraph { result.push(current); for (const child of this.forwardEdges.get(current) || []) { - if (!subgraph.has(child)) continue; + if (!inDegree.has(child)) continue; const inDegreeValue = inDegree.get(child); if (inDegreeValue === undefined) { log.error("::hashgraph::Kahn: Undefined in-degree value"); diff --git a/packages/object/tests/hashgraph.test.ts b/packages/object/tests/hashgraph.test.ts index 19706c74..7f3802fc 100644 --- a/packages/object/tests/hashgraph.test.ts +++ b/packages/object/tests/hashgraph.test.ts @@ -49,8 +49,8 @@ describe("HashGraph construction tests", () => { const linearOps = obj2.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ - { type: "add", value: 1 }, { type: "add", value: 2 }, + { type: "add", value: 1 }, ]); }); @@ -170,8 +170,8 @@ describe("HashGraph for AddWinSet tests", () => { const linearOps = obj1.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ { type: "add", value: 1 }, - { type: "add", value: 2 }, { type: "remove", value: 1 }, + { type: "add", value: 2 }, ]); }); @@ -204,8 +204,8 @@ describe("HashGraph for AddWinSet tests", () => { expect(linearOps).toEqual([ { type: "add", value: 1 }, { type: "add", value: 1 }, - { type: "remove", value: 5 }, { type: "add", value: 10 }, + { type: "remove", value: 5 }, ]); }); @@ -235,9 +235,9 @@ describe("HashGraph for AddWinSet tests", () => { const linearOps = obj1.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ - { type: "add", value: 1 }, { type: "add", value: 1 }, { type: "add", value: 2 }, + { type: "add", value: 1 }, ]); }); @@ -288,12 +288,12 @@ describe("HashGraph for AddWinSet tests", () => { const linearOps = obj1.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ + { type: "add", value: 1 }, { type: "add", value: 1 }, { type: "remove", value: 2 }, { type: "add", value: 2 }, - { type: "add", value: 1 }, - { type: "add", value: 3 }, { type: "remove", value: 1 }, + { type: "add", value: 3 }, ]); }); @@ -345,11 +345,11 @@ describe("HashGraph for AddWinSet tests", () => { const linearOps = obj1.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ { type: "add", value: 1 }, - { type: "remove", value: 2 }, { type: "add", value: 1 }, { type: "remove", value: 2 }, - { type: "add", value: 3 }, + { type: "remove", value: 2 }, { type: "remove", value: 1 }, + { type: "add", value: 3 }, { type: "add", value: 2 }, { type: "remove", value: 1 }, ]); @@ -602,7 +602,7 @@ describe("Vertex timestamp tests", () => { test("Test: Vertex's timestamp must not be less than any of its dependencies' timestamps", () => { /* __ V1:ADD(1) __ - / \ + / \ ROOT -- V2:ADD(2) ---- V4:ADD(4) (invalid) \ / -- V3:ADD(3) -- From 3f2f9517728512379260e6e8b196178a0ba26a9b Mon Sep 17 00:00:00 2001 From: winprn Date: Thu, 2 Jan 2025 14:30:27 +0700 Subject: [PATCH 07/10] fix: revert changes in comment --- packages/object/tests/hashgraph.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/object/tests/hashgraph.test.ts b/packages/object/tests/hashgraph.test.ts index 7f3802fc..77989622 100644 --- a/packages/object/tests/hashgraph.test.ts +++ b/packages/object/tests/hashgraph.test.ts @@ -602,7 +602,7 @@ describe("Vertex timestamp tests", () => { test("Test: Vertex's timestamp must not be less than any of its dependencies' timestamps", () => { /* __ V1:ADD(1) __ - / \ + / \ ROOT -- V2:ADD(2) ---- V4:ADD(4) (invalid) \ / -- V3:ADD(3) -- From 2c0f722ca1d5750f5581f522e21e307721e878e7 Mon Sep 17 00:00:00 2001 From: winprn Date: Thu, 2 Jan 2025 14:51:41 +0700 Subject: [PATCH 08/10] improv: speed up vertex checking --- packages/object/src/hashgraph/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index e8348951..8415b7e3 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -266,9 +266,9 @@ export class HashGraph { } for (const [vertex, children] of this.forwardEdges) { - if (!subgraph.has(vertex)) continue; + if (!inDegree.has(vertex)) continue; for (const child of children) { - if (!subgraph.has(child)) continue; + if (!inDegree.has(child)) continue; inDegree.set(child, (inDegree.get(child) || 0) + 1); } } From 5adaf37f3623667e5ff86227a0ff5c60d455136e Mon Sep 17 00:00:00 2001 From: winprn Date: Thu, 2 Jan 2025 15:07:53 +0700 Subject: [PATCH 09/10] apply suggestions --- packages/object/src/hashgraph/index.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index 8415b7e3..7ab90062 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -256,7 +256,7 @@ export class HashGraph { return result; } - kahnAlgorithm(origin: Hash, subgraph: ObjectSet): Hash[] { + kahnsAlgorithm(origin: Hash, subgraph: ObjectSet): Hash[] { const result: Hash[] = []; const inDegree = new Map(); const queue: Hash[] = []; @@ -284,13 +284,9 @@ export class HashGraph { for (const child of this.forwardEdges.get(current) || []) { if (!inDegree.has(child)) continue; - const inDegreeValue = inDegree.get(child); - if (inDegreeValue === undefined) { - log.error("::hashgraph::Kahn: Undefined in-degree value"); - return []; - } + const inDegreeValue = inDegree.get(child) || 0; inDegree.set(child, inDegreeValue - 1); - if (inDegree.get(child) === 0) { + if (inDegreeValue - 1 === 0) { queue.push(child); } } @@ -310,7 +306,7 @@ export class HashGraph { origin: Hash = HashGraph.rootHash, subgraph: ObjectSet = new ObjectSet(this.vertices.keys()), ): Hash[] { - const result = this.kahnAlgorithm(origin, subgraph); + const result = this.kahnsAlgorithm(origin, subgraph); if (!updateBitsets) return result; this.reachablePredecessors.clear(); this.topoSortedIndex.clear(); From d040be7207b3226378bff67099f178ed8d9e7d58 Mon Sep 17 00:00:00 2001 From: winprn Date: Fri, 3 Jan 2025 11:44:02 +0700 Subject: [PATCH 10/10] improv: remove DFS from codebase --- packages/object/src/hashgraph/index.ts | 51 ++------------------------ 1 file changed, 3 insertions(+), 48 deletions(-) diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index 7ab90062..12a45cef 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -14,12 +14,6 @@ export type { Vertex, Operation }; export type Hash = string; -export enum DepthFirstSearchState { - UNVISITED = 0, - VISITING = 1, - VISITED = 2, -} - export enum OperationType { NOP = "-1", } @@ -219,43 +213,6 @@ export class HashGraph { return hash; } - depthFirstSearch( - origin: Hash, - subgraph: ObjectSet, - visited: Map = new Map(), - ): Hash[] { - const result: Hash[] = []; - for (const hash of subgraph.entries()) { - visited.set(hash, DepthFirstSearchState.UNVISITED); - } - const visit = (hash: Hash) => { - visited.set(hash, DepthFirstSearchState.VISITING); - - const children = this.forwardEdges.get(hash) || []; - for (const child of children) { - if (!subgraph.has(child)) continue; - if (visited.get(child) === DepthFirstSearchState.VISITING) { - log.error("::hashgraph::DFS: Cycle detected"); - return; - } - if (visited.get(child) === undefined) { - log.error("::hashgraph::DFS: Undefined child"); - return; - } - if (visited.get(child) === DepthFirstSearchState.UNVISITED) { - visit(child); - } - } - - result.push(hash); - visited.set(hash, DepthFirstSearchState.VISITED); - }; - - visit(origin); - - return result; - } - kahnsAlgorithm(origin: Hash, subgraph: ObjectSet): Hash[] { const result: Hash[] = []; const inDegree = new Map(); @@ -511,18 +468,16 @@ export class HashGraph { } } - const visited = new Map(); - this.depthFirstSearch( + const topoOrder = this.kahnsAlgorithm( HashGraph.rootHash, new ObjectSet(this.vertices.keys()), - visited, ); + for (const vertex of this.getAllVertices()) { - if (!visited.has(vertex.hash)) { + if (!topoOrder.includes(vertex.hash)) { return false; } } - return true; }