diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index ce251bcb7..e23d91c24 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -132,35 +132,8 @@ export class HashGraph { this.arePredecessorsFresh = false; } - /* Add a vertex to the hashgraph with the given operation and dependencies. - * If the vertex already exists, return the hash of the existing vertex. - * Throws an error if any of the dependencies are not present in the hashgraph. - */ + // Add a new vertex to the hashgraph. addVertex(vertex: Vertex) { - if (this.vertices.has(vertex.hash)) { - return; // Vertex already exists - } - - if (vertex.dependencies.length === 0) { - throw new Error("Vertex dependencies are empty."); - } - for (const dep of vertex.dependencies) { - const depVertex = this.vertices.get(dep); - if (depVertex === undefined) { - throw new Error("Invalid dependency detected."); - } - if (depVertex.timestamp > vertex.timestamp) { - // Vertex's timestamp must not be less than any of its dependencies' timestamps - throw new Error("Invalid timestamp detected."); - } - } - - const currentTimestamp = Date.now(); - if (vertex.timestamp > currentTimestamp) { - // Vertex created in the future is invalid - throw new Error("Invalid timestamp detected."); - } - this.vertices.set(vertex.hash, vertex); this.frontier.push(vertex.hash); // Update forward edges diff --git a/packages/object/src/index.ts b/packages/object/src/index.ts index 771194668..99128a6c9 100644 --- a/packages/object/src/index.ts +++ b/packages/object/src/index.ts @@ -242,6 +242,40 @@ export class DRPObject implements ObjectPb.DRPObjectBase { return this._mergeWithDrp(vertices); } + validateVertex(vertex: Vertex) { + // Validate hash value + if ( + vertex.hash !== + computeHash(vertex.peerId, vertex.operation, vertex.dependencies, vertex.timestamp) + ) { + throw new Error(`Invalid hash for vertex ${vertex.hash}`); + } + + // Validate vertex dependencies + if (vertex.dependencies.length === 0) { + throw new Error(`Vertex ${vertex.hash} has no dependencies.`); + } + for (const dep of vertex.dependencies) { + const depVertex = this.hashGraph.vertices.get(dep); + if (depVertex === undefined) { + throw new Error(`Vertex ${vertex.hash} has invalid dependency ${dep}.`); + } + if (depVertex.timestamp > vertex.timestamp) { + // Vertex's timestamp must not be less than any of its dependencies' timestamps + throw new Error(`Vertex ${vertex.hash} has invalid timestamp.`); + } + } + if (vertex.timestamp > Date.now()) { + // Vertex created in the future is invalid + throw new Error(`Vertex ${vertex.hash} has invalid timestamp.`); + } + + // Validate writer permission + if (!this._checkWriterPermission(vertex.peerId, vertex.dependencies)) { + throw new Error(`Vertex ${vertex.peerId} does not have write permission.`); + } + } + /* Merges the vertices into the hashgraph using DRP */ private _mergeWithDrp(vertices: Vertex[]): [merged: boolean, missing: string[]] { @@ -254,34 +288,19 @@ export class DRPObject implements ObjectPb.DRPObjectBase { } try { - const acl = this._computeObjectACL(vertex.dependencies); - if (!acl.query_isWriter(vertex.peerId)) { - throw new Error(`${vertex.peerId} does not have write permission.`); - } - if ( - vertex.hash !== - computeHash(vertex.peerId, vertex.operation, vertex.dependencies, vertex.timestamp) - ) { - throw new Error(`Invalid hash for vertex ${vertex.hash}`); - } + this.validateVertex(vertex); const preComputeLca = this.computeLCA(vertex.dependencies); if (vertex.operation.drpType === DrpType.DRP) { - const drp = this._computeDRP(vertex.dependencies, preComputeLca); - this.hashGraph.addVertex(vertex); - this._applyOperation(drp, vertex.operation); - + const drp = this._computeDRP(vertex.dependencies, preComputeLca, vertex.operation); this._setObjectACLState(vertex, preComputeLca); this._setDRPState(vertex, preComputeLca, this._getDRPState(drp)); } else { - const acl = this._computeObjectACL(vertex.dependencies, preComputeLca); - - this.hashGraph.addVertex(vertex); - this._applyOperation(acl, vertex.operation); - + const acl = this._computeObjectACL(vertex.dependencies, preComputeLca, vertex.operation); this._setObjectACLState(vertex, preComputeLca, this._getDRPState(acl)); this._setDRPState(vertex, preComputeLca); } + this.hashGraph.addVertex(vertex); this._initializeFinalityState(vertex.hash); newVertices.push(vertex); } catch (_) { @@ -306,9 +325,7 @@ export class DRPObject implements ObjectPb.DRPObjectBase { } try { - if (!this._checkWriterPermission(vertex.peerId)) { - return [false, [vertex.hash]]; - } + this.validateVertex(vertex); this.hashGraph.addVertex({ hash: vertex.hash, operation: vertex.operation, @@ -354,8 +371,13 @@ export class DRPObject implements ObjectPb.DRPObjectBase { } // check if the given peer has write permission - private _checkWriterPermission(peerId: string): boolean { - return this.acl ? (this.acl as ACL).query_isWriter(peerId) : true; + private _checkWriterPermission(peerId: string, deps: Hash[]): boolean { + if (!this.drp) { + return (this.acl as ACL).query_isWriter(peerId); + } + + const acl = this._computeObjectACL(deps); + return (acl as ACL).query_isWriter(peerId); } // apply the operation to the DRP @@ -572,9 +594,9 @@ export class DRPObject implements ObjectPb.DRPObjectBase { } } -export function computeHash( +function computeHash( peerId: string, - operation: Operation, + operation: Operation | undefined, deps: Hash[], timestamp: number ): Hash { diff --git a/packages/object/tests/drpobject.test.ts b/packages/object/tests/drpobject.test.ts index ebc6e3bb1..38d70e228 100644 --- a/packages/object/tests/drpobject.test.ts +++ b/packages/object/tests/drpobject.test.ts @@ -1,5 +1,5 @@ import { SetDRP } from "@ts-drp/blueprints/src/index.js"; -import { beforeEach, describe, expect, it, test } from "vitest"; +import { beforeEach, describe, expect, it, test, vi } from "vitest"; import { DRPObject, ObjectACL } from "../src/index.js"; @@ -69,3 +69,36 @@ describe("Drp Object should be able to change state value", () => { } }); }); + +describe("Merging vertices tests", () => { + let obj1: DRPObject; + let obj2: DRPObject; + + beforeEach(() => { + obj1 = new DRPObject({ peerId: "peer1", acl, drp: new SetDRP() }); + obj2 = new DRPObject({ peerId: "peer2", acl, drp: new SetDRP() }); + }); + + test("Test: merge should skip unknown dependencies", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(Date.UTC(1998, 11, 19))); + const drp1 = obj1.drp as SetDRP; + const drp2 = obj2.drp as SetDRP; + + drp1.add(1); + drp2.add(2); + obj1.merge(obj2.hashGraph.getAllVertices()); + drp1.add(3); + + const vertex = obj1.vertices.find( + (v) => v.operation?.opType === "add" && v.operation.value[0] === 3 + ); + if (!vertex) { + throw new Error("Vertex not found"); + } + expect(obj2.merge([vertex])).toEqual([ + false, + ["e5ef52c6186abe51635619df8bc8676c19f5a6519e40f47072683437255f026a"], + ]); + }); +}); diff --git a/packages/object/tests/hashgraph.test.ts b/packages/object/tests/hashgraph.test.ts index 914eb3758..fd31cf57f 100644 --- a/packages/object/tests/hashgraph.test.ts +++ b/packages/object/tests/hashgraph.test.ts @@ -125,19 +125,18 @@ describe("HashGraph construction tests", () => { new Uint8Array() ); expect(() => { - obj1.hashGraph.addVertex(fakeRoot); - }).toThrowError("Vertex dependencies are empty."); + obj1.validateVertex(fakeRoot); + }).toThrowError(`Vertex ${fakeRoot.hash} has no dependencies.`); + const vertex = newVertex( + "peer1", + { opType: "add", value: [1], drpType: DrpType.DRP }, + [fakeRoot.hash], + Date.now(), + new Uint8Array() + ); expect(() => { - obj1.hashGraph.addVertex( - newVertex( - "peer1", - { opType: "add", value: [1], drpType: DrpType.DRP }, - [fakeRoot.hash], - Date.now(), - new Uint8Array() - ) - ); - }).toThrowError("Invalid dependency detected."); + obj1.validateVertex(vertex); + }).toThrowError(`Vertex ${vertex.hash} has invalid dependency ${fakeRoot.hash}.`); expect(selfCheckConstraints(obj1.hashGraph)).toBe(true); const linearOps = obj1.hashGraph.linearizeOperations(); @@ -564,17 +563,16 @@ describe("Vertex timestamp tests", () => { drp1.add(1); - expect(() => - obj1.hashGraph.addVertex( - newVertex( - "peer1", - { opType: "add", value: [1], drpType: DrpType.DRP }, - obj1.hashGraph.getFrontier(), - Number.POSITIVE_INFINITY, - new Uint8Array() - ) - ) - ).toThrowError("Invalid timestamp detected."); + const vertex = newVertex( + "peer1", + { opType: "add", value: [1], drpType: DrpType.DRP }, + obj1.hashGraph.getFrontier(), + Number.POSITIVE_INFINITY, + new Uint8Array() + ); + expect(() => obj1.validateVertex(vertex)).toThrowError( + `Vertex ${vertex.hash} has invalid timestamp.` + ); }); test("Test: Vertex's timestamp must not be less than any of its dependencies' timestamps", () => { @@ -597,21 +595,20 @@ describe("Vertex timestamp tests", () => { obj1.merge(obj2.hashGraph.getAllVertices()); obj1.merge(obj3.hashGraph.getAllVertices()); - expect(() => - obj1.hashGraph.addVertex( - newVertex( - "peer1", - { - opType: "add", - value: [1], - drpType: DrpType.DRP, - }, - obj1.hashGraph.getFrontier(), - 1, - new Uint8Array() - ) - ) - ).toThrowError("Invalid timestamp detected."); + const vertex = newVertex( + "peer1", + { + opType: "add", + value: [1], + drpType: DrpType.DRP, + }, + obj1.hashGraph.getFrontier(), + 1, + new Uint8Array() + ); + expect(() => obj1.validateVertex(vertex)).toThrowError( + `Vertex ${vertex.hash} has invalid timestamp.` + ); }); });