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: fix root hash, add DAG testcases, DFS function #220

Merged
merged 9 commits into from
Nov 1, 2024
86 changes: 74 additions & 12 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 Down Expand Up @@ -149,26 +158,44 @@ 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;
}

// Time complexity: O(V + E), Space complexity: O(V)
topologicalSort(updateBitsets = false): Hash[] {
this.reachablePredecessors.clear();
this.topoSortedIndex.clear();

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

if (!updateBitsets) return result;
Expand Down Expand Up @@ -258,6 +285,41 @@ 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;
}

// Time complexity: O(1), Space complexity: O(1)
areCausallyRelatedUsingBFS(hash1: Hash, hash2: Hash): boolean {
return (
this._areCausallyRelatedUsingBFS(hash1, hash2) ||
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