Skip to content

Commit

Permalink
feat: fix root hash, add DAG testcases, DFS function (#220)
Browse files Browse the repository at this point in the history
  • Loading branch information
hoangquocvietuet authored Nov 1, 2024
1 parent fe9d267 commit a89a50f
Show file tree
Hide file tree
Showing 3 changed files with 727 additions and 608 deletions.
92 changes: 72 additions & 20 deletions packages/object/src/hashgraph/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as crypto from "node:crypto";
import { Logger } from "@topology-foundation/logger";
import { linearizeMultiple } from "../linearize/multipleSemantics.js";
import { linearizePair } from "../linearize/pairSemantics.js";
import {
Expand All @@ -7,11 +8,19 @@ import {
} from "../proto/topology/object/object_pb.js";
import { BitSet } from "./bitset.js";

const log: Logger = new Logger("hashgraph");

// Reexporting the Vertex and Operation types from the protobuf file
export { Vertex, Operation };

export type Hash = string;

export enum DepthFirstSearchState {
UNVISITED = 0,
VISITING = 1,
VISITED = 2,
}

export enum OperationType {
NOP = "-1",
}
Expand Down Expand Up @@ -51,7 +60,7 @@ export class HashGraph {
)
*/
static readonly rootHash: Hash =
"ee075937c2a6c8ccf8d94fb2a130c596d3dbcc32910b6e744ad55c3e41b41ad6";
"02465e287e3d086f12c6edd856953ca5ad0f01d6707bf8e410b4a601314c1ca5";
private arePredecessorsFresh = false;
private reachablePredecessors: Map<Hash, BitSet> = new Map();
private topoSortedIndex: Map<Hash, number> = new Map();
Expand All @@ -67,7 +76,6 @@ export class HashGraph {
this.resolveConflicts = resolveConflicts;
this.semanticsType = semanticsType;

// Create and add the NOP root vertex
const rootVertex: Vertex = {
hash: HashGraph.rootHash,
nodeId: "",
Expand Down Expand Up @@ -110,8 +118,6 @@ export class HashGraph {
return vertex;
}

// Time complexity: O(d), where d is the number of dependencies
// Space complexity: O(d)
addVertex(operation: Operation, deps: Hash[], nodeId: string): Hash {
const hash = computeHash(nodeId, operation, deps);
if (this.vertices.has(hash)) {
Expand Down Expand Up @@ -149,26 +155,43 @@ export class HashGraph {
return hash;
}

// Time complexity: O(V + E), Space complexity: O(V)
topologicalSort(updateBitsets = false): Hash[] {
depthFirstSearch(visited: Map<Hash, number> = new Map()): Hash[] {
const result: Hash[] = [];
const visited = new Set<Hash>();
this.reachablePredecessors.clear();
this.topoSortedIndex.clear();

for (const vertex of this.getAllVertices()) {
visited.set(vertex.hash, DepthFirstSearchState.UNVISITED);
}
const visit = (hash: Hash) => {
if (visited.has(hash)) return;

visited.add(hash);
visited.set(hash, DepthFirstSearchState.VISITING);

const children = this.forwardEdges.get(hash) || [];
for (const child of children) {
visit(child);
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);
};
// Start with the root vertex

visit(HashGraph.rootHash);

return result;
}

topologicalSort(updateBitsets = false): Hash[] {
this.reachablePredecessors.clear();
this.topoSortedIndex.clear();

const result = this.depthFirstSearch();
result.reverse();

if (!updateBitsets) return result;
Expand Down Expand Up @@ -210,7 +233,6 @@ export class HashGraph {
}
}

// Amortised time complexity: O(1), Amortised space complexity: O(1)
areCausallyRelatedUsingBitsets(hash1: Hash, hash2: Hash): boolean {
if (!this.arePredecessorsFresh) {
this.topologicalSort(true);
Expand Down Expand Up @@ -258,6 +280,40 @@ export class HashGraph {
return false;
}

selfCheckConstraints(): boolean {
const degree = new Map<Hash, number>();
for (const vertex of this.getAllVertices()) {
const hash = vertex.hash;
degree.set(hash, 0);
}
for (const [_, children] of this.forwardEdges) {
for (const child of children) {
degree.set(child, (degree.get(child) || 0) + 1);
}
}
for (const vertex of this.getAllVertices()) {
const hash = vertex.hash;
if (degree.get(hash) !== vertex.dependencies.length) {
return false;
}
if (vertex.dependencies.length === 0) {
if (hash !== HashGraph.rootHash) {
return false;
}
}
}

const visited = new Map<Hash, number>();
this.depthFirstSearch(visited);
for (const vertex of this.getAllVertices()) {
if (!visited.has(vertex.hash)) {
return false;
}
}

return true;
}

areCausallyRelatedUsingBFS(hash1: Hash, hash2: Hash): boolean {
return (
this._areCausallyRelatedUsingBFS(hash1, hash2) ||
Expand All @@ -269,23 +325,19 @@ export class HashGraph {
return Array.from(this.frontier);
}

// Time complexity: O(1), Space complexity: O(1)
getDependencies(vertexHash: Hash): Hash[] {
return Array.from(this.vertices.get(vertexHash)?.dependencies || []);
}

// Time complexity: O(1), Space complexity: O(1)
getVertex(hash: Hash): Vertex | undefined {
return this.vertices.get(hash);
}

// Time complexity: O(V), Space complexity: O(V)
getAllVertices(): Vertex[] {
return Array.from(this.vertices.values());
}
}

// Time complexity: O(1), Space complexity: O(1)
function computeHash<T>(
nodeId: string,
operation: Operation,
Expand Down
138 changes: 100 additions & 38 deletions packages/object/tests/hashgraph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,68 @@ import { AddWinsSet } from "../../blueprints/src/AddWinsSet/index.js";
import { PseudoRandomWinsSet } from "../../blueprints/src/PseudoRandomWinsSet/index.js";
import { type Operation, OperationType, TopologyObject } from "../src/index.js";

describe("HashGraph construction tests", () => {
let obj1: TopologyObject;
let obj2: TopologyObject;

beforeEach(async () => {
obj1 = new TopologyObject("peer1", new AddWinsSet<number>());
obj2 = new TopologyObject("peer2", new AddWinsSet<number>());
});

test("Test: HashGraph should be DAG compatibility", () => {
/* - V1:ADD(1)
root /
\ - V2:ADD(2)
*/
const cro1 = obj1.cro as AddWinsSet<number>;
const cro2 = obj2.cro as AddWinsSet<number>;

cro1.add(1);
cro2.add(2);

obj2.merge(obj1.hashGraph.getAllVertices());

expect(obj2.hashGraph.selfCheckConstraints()).toBe(true);

const linearOps = obj2.hashGraph.linearizeOperations();
expect(linearOps).toEqual([
{ type: "add", value: 1 },
{ type: "add", value: 2 },
]);
});

test("Test: HashGraph has 2 root vertices", () => {
/*
root - V1:ADD(1)
fakeRoot - V2:ADD(1)
*/
const cro1 = obj1.cro as AddWinsSet<number>;
cro1.add(1);
// add fake root
const hash = obj1.hashGraph.addVertex(
{
type: "root",
value: null,
},
[],
"",
);
obj1.hashGraph.addVertex(
{
type: "add",
value: 1,
},
[hash],
"",
);
expect(obj1.hashGraph.selfCheckConstraints()).toBe(false);

const linearOps = obj1.hashGraph.linearizeOperations();
expect(linearOps).toEqual([{ type: "add", value: 1 }]);
});
});

describe("HashGraph for AddWinSet tests", () => {
let obj1: TopologyObject;
let obj2: TopologyObject;
Expand All @@ -16,8 +78,8 @@ describe("HashGraph for AddWinSet tests", () => {

test("Test: Add Two Vertices", () => {
/*
V1:NOP <- V2:ADD(1) <- V2:REMOVE(1)
*/
V1:NOP <- V2:ADD(1) <- V2:REMOVE(1)
*/

const cro1 = obj1.cro as AddWinsSet<number>;
cro1.add(1);
Expand All @@ -33,10 +95,10 @@ describe("HashGraph for AddWinSet tests", () => {

test("Test: Add Two Concurrent Vertices With Same Value", () => {
/*
_ V2:REMOVE(1)
V1:ADD(1) /
\ _ V3:ADD(1)
*/
_ V2:REMOVE(1)
V1:ADD(1) /
\ _ V3:ADD(1)
*/

const cro1 = obj1.cro as AddWinsSet<number>;
const cro2 = obj2.cro as AddWinsSet<number>;
Expand All @@ -61,10 +123,10 @@ describe("HashGraph for AddWinSet tests", () => {

test("Test: Add Two Concurrent Vertices With Different Values", () => {
/*
_ V2:REMOVE(1)
V1:ADD(1) /
\ _ V3:ADD(2)
*/
_ V2:REMOVE(1)
V1:ADD(1) /
\ _ V3:ADD(2)
*/

const cro1 = obj1.cro as AddWinsSet<number>;
const cro2 = obj2.cro as AddWinsSet<number>;
Expand All @@ -91,10 +153,10 @@ describe("HashGraph for AddWinSet tests", () => {

test("Test: Tricky Case", () => {
/*
___ V2:REMOVE(1) <- V4:ADD(10)
V1:ADD(1) /
\ ___ V3:ADD(1) <- V5:REMOVE(5)
*/
___ V2:REMOVE(1) <- V4:ADD(10)
V1:ADD(1) /
\ ___ V3:ADD(1) <- V5:REMOVE(5)
*/

const cro1 = obj1.cro as AddWinsSet<number>;
const cro2 = obj2.cro as AddWinsSet<number>;
Expand Down Expand Up @@ -125,10 +187,10 @@ describe("HashGraph for AddWinSet tests", () => {

test("Test: Yuta Papa's Case", () => {
/*
___ V2:REMOVE(1) <- V4:ADD(2)
V1:ADD(1) /
\ ___ V3:REMOVE(2) <- V5:ADD(1)
*/
___ V2:REMOVE(1) <- V4:ADD(2)
V1:ADD(1) /
\ ___ V3:REMOVE(2) <- V5:ADD(1)
*/

const cro1 = obj1.cro as AddWinsSet<number>;
const cro2 = obj2.cro as AddWinsSet<number>;
Expand Down Expand Up @@ -157,14 +219,14 @@ describe("HashGraph for AddWinSet tests", () => {

test("Test: Mega Complex Case", () => {
/*
__ V6:ADD(3)
/
___ V2:ADD(1) <-- V3:RM(2) <-- V7:RM(1) <-- V8:RM(3)
/ ______________/
V1:ADD(1)/ /
\ /
\ ___ V4:RM(2) <-- V5:ADD(2) <-- V9:RM(1)
*/
__ V6:ADD(3)
/
___ V2:ADD(1) <-- V3:RM(2) <-- V7:RM(1) <-- V8:RM(3)
/ ______________/
V1:ADD(1)/ /
\ /
\ ___ V4:RM(2) <-- V5:ADD(2) <-- V9:RM(1)
*/

const cro1 = obj1.cro as AddWinsSet<number>;
const cro2 = obj2.cro as AddWinsSet<number>;
Expand Down Expand Up @@ -212,14 +274,14 @@ describe("HashGraph for AddWinSet tests", () => {

test("Test: Mega Complex Case 1", () => {
/*
__ V5:ADD(3)
/
___ V2:ADD(1) <-- V3:RM(2) <-- V6:RM(1) <-- V8:RM(3)
/ ^
V1:ADD(1)/ \
\ \
\ ___ V4:RM(2) <-------------------- V7:ADD(2) <-- V9:RM(1)
*/
__ V5:ADD(3)
/
___ V2:ADD(1) <-- V3:RM(2) <-- V6:RM(1) <-- V8:RM(3)
/ ^
V1:ADD(1)/ \
\ \
\ ___ V4:RM(2) <-------------------- V7:ADD(2) <-- V9:RM(1)
*/

const cro1 = obj1.cro as AddWinsSet<number>;
const cro2 = obj2.cro as AddWinsSet<number>;
Expand Down Expand Up @@ -269,10 +331,10 @@ describe("HashGraph for AddWinSet tests", () => {

test("Test: Joao's latest brain teaser", () => {
/*
__ V2:Add(2) <------------\
V1:Add(1) / \ - V5:RM(2)
\__ V3:RM(2) <- V4:RM(2) <--/
*/
__ V2:Add(2) <------------\
V1:Add(1) / \ - V5:RM(2)
\__ V3:RM(2) <- V4:RM(2) <--/
*/

const cro1 = obj1.cro as AddWinsSet<number>;
const cro2 = obj2.cro as AddWinsSet<number>;
Expand Down
Loading

0 comments on commit a89a50f

Please sign in to comment.