From 1beba5ec79fdf959e6282f5b5fa04489e95b3df2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Dec 2024 09:49:31 +0000 Subject: [PATCH 1/3] chore(deps): bump commander from 12.1.0 to 13.0.0 (#308) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packages/node/package.json | 2 +- pnpm-lock.yaml | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/node/package.json b/packages/node/package.json index 6b75bac4..117a9ad6 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -42,7 +42,7 @@ "@ts-drp/network": "0.4.4", "@ts-drp/object": "0.4.4", "@ts-drp/logger": "0.4.4", - "commander": "^12.1.0", + "commander": "^13.0.0", "uint8arrays": "^5.1.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8263bd69..821decd4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -258,8 +258,8 @@ importers: specifier: 0.4.4 version: link:../object commander: - specifier: ^12.1.0 - version: 12.1.0 + specifier: ^13.0.0 + version: 13.0.0 dotenv: specifier: ^16.4.5 version: 16.4.7 @@ -2559,6 +2559,10 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@13.0.0: + resolution: {integrity: sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==} + engines: {node: '>=18'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -7973,6 +7977,8 @@ snapshots: commander@12.1.0: {} + commander@13.0.0: {} + commander@2.20.3: {} commondir@1.0.1: {} From 79133d09829d77fca48612a7850774d4944b19ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=E1=BA=A1m=20Xu=C3=A2n=20Trung?= <66519569+trungnotchung@users.noreply.github.com> Date: Thu, 2 Jan 2025 22:00:39 +0700 Subject: [PATCH 2/3] feat: handle acl operations (#311) Co-authored-by: anhnd350309 <156870690+anhnd350309@users.noreply.github.com> --- packages/object/src/index.ts | 56 +++++++++++++++----- packages/object/tests/hashgraph.test.ts | 68 ++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 14 deletions(-) diff --git a/packages/object/src/index.ts b/packages/object/src/index.ts index 7f57948d..d7f285d6 100644 --- a/packages/object/src/index.ts +++ b/packages/object/src/index.ts @@ -102,23 +102,35 @@ export class DRPObject implements IDRPObject { } // This function is black magic, it allows us to intercept calls to the DRP object - proxyDRPHandler(): ProxyHandler { + proxyDRPHandler(parentProp?: string): ProxyHandler { const obj = this; return { get(target, propKey, receiver) { - if (typeof target[propKey as keyof object] === "function") { + const value = Reflect.get(target, propKey, receiver); + + if (typeof value === "function") { + const fullPropKey = parentProp + ? `${parentProp}.${String(propKey)}` + : String(propKey); return new Proxy(target[propKey as keyof object], { apply(applyTarget, thisArg, args) { if ((thisArg.operations as string[]).includes(propKey as string)) - obj.callFn( - propKey as string, - args.length === 1 ? args[0] : args, - ); + obj.callFn(fullPropKey, args.length === 1 ? args[0] : args); return Reflect.apply(applyTarget, thisArg, args); }, }); } - return Reflect.get(target, propKey, receiver); + + if (typeof value === "object" && value !== null && propKey === "acl") { + return new Proxy( + value, + obj.proxyDRPHandler( + parentProp ? `${parentProp}.${String(propKey)}` : String(propKey), + ), + ); + } + + return value; }, }; } @@ -184,6 +196,28 @@ export class DRPObject implements IDRPObject { } } + private _applyOperation(drp: DRP, operation: Operation) { + const { type, value } = operation; + + const typeParts = type.split("."); + // biome-ignore lint: target can be anything + let target: any = drp; + for (let i = 0; i < typeParts.length - 1; i++) { + target = target[typeParts[i]]; + if (!target) { + throw new Error(`Invalid operation type: ${type}`); + } + } + + const methodName = typeParts[typeParts.length - 1]; + if (typeof target[methodName] !== "function") { + throw new Error(`${type} is not a function`); + } + + const args = Array.isArray(value) ? value : [value]; + target[methodName](...args); + } + private _computeState( vertexDependencies: Hash[], vertexOperation?: Operation | undefined, @@ -216,14 +250,10 @@ export class DRPObject implements IDRPObject { } for (const op of linearizedOperations) { - const args = Array.isArray(op.value) ? op.value : [op.value]; - drp[op.type](...args); + this._applyOperation(drp, op); } if (vertexOperation) { - const args = Array.isArray(vertexOperation.value) - ? vertexOperation.value - : [vertexOperation.value]; - drp[vertexOperation.type](...args); + this._applyOperation(drp, vertexOperation); } const varNames: string[] = Object.keys(drp); diff --git a/packages/object/tests/hashgraph.test.ts b/packages/object/tests/hashgraph.test.ts index 19706c74..dc681947 100644 --- a/packages/object/tests/hashgraph.test.ts +++ b/packages/object/tests/hashgraph.test.ts @@ -1,6 +1,6 @@ +import { AddWinsSetWithACL } from "@topology-foundation/blueprints/src/AddWinsSetWithACL/index.js"; import { beforeEach, describe, expect, test } from "vitest"; import { AddWinsSet } from "../../blueprints/src/AddWinsSet/index.js"; -import { PseudoRandomWinsSet } from "../../blueprints/src/PseudoRandomWinsSet/index.js"; import { DRPObject, type Operation, OperationType } from "../src/index.js"; describe("HashGraph construction tests", () => { @@ -633,3 +633,69 @@ describe("Vertex timestamp tests", () => { ).toThrowError("Invalid timestamp detected."); }); }); + +describe("Operation with ACL tests", () => { + let obj1: DRPObject; + let obj2: DRPObject; + + beforeEach(async () => { + const peerIdToPublicKey = new Map([ + ["peer1", "publicKey1"], + ]); + obj1 = new DRPObject( + "peer1", + new AddWinsSetWithACL(peerIdToPublicKey), + ); + obj2 = new DRPObject( + "peer2", + new AddWinsSetWithACL(peerIdToPublicKey), + ); + }); + + test("Node with admin permission can grant permission to other nodes", () => { + /* + ROOT -- V1:GRANT("peer2") + */ + + const drp1 = obj1.drp as AddWinsSetWithACL; + const drp2 = obj2.drp as AddWinsSetWithACL; + + drp1.acl.grant("peer1", "peer2", "publicKey2"); + obj2.merge(obj1.hashGraph.getAllVertices()); + expect(drp2.acl.isWriter("peer2")).toBe(true); + }); + + test("Node with writer permission can create vertices", () => { + /* + ROOT -- V1:GRANT("peer2") -- V2:ADD(1) + */ + const drp1 = obj1.drp as AddWinsSetWithACL; + const drp2 = obj2.drp as AddWinsSetWithACL; + + drp1.acl.grant("peer1", "peer2", "publicKey2"); + obj2.merge(obj1.hashGraph.getAllVertices()); + + drp2.add("peer2", 1); + obj1.merge(obj2.hashGraph.getAllVertices()); + expect(drp1.contains(1)).toBe(true); + }); + + test("Revoke permission from writer", () => { + /* + ROOT -- V1:GRANT("peer2") -- V2:ADD(1) -- V3:REVOKE("peer2") + */ + const drp1 = obj1.drp as AddWinsSetWithACL; + const drp2 = obj2.drp as AddWinsSetWithACL; + + drp1.acl.grant("peer1", "peer2", "publicKey2"); + obj2.merge(obj1.hashGraph.getAllVertices()); + + expect(drp2.acl.isWriter("peer2")).toBe(true); + drp2.add("peer2", 1); + + obj1.merge(obj2.hashGraph.getAllVertices()); + drp1.acl.revoke("peer1", "peer2"); + obj2.merge(obj1.hashGraph.getAllVertices()); + expect(drp2.acl.isWriter("peer2")).toBe(false); + }); +}); From ce3277b509a8918e8bae77a2bac17b21a3d66e1d Mon Sep 17 00:00:00 2001 From: Ly Dinh Minh Man <85622996+winprn@users.noreply.github.com> Date: Fri, 3 Jan 2025 12:12:48 +0700 Subject: [PATCH 3/3] feat: use Kahn's algorithm for finding toposort (#312) --- packages/object/src/hashgraph/index.ts | 74 ++++++++++++------------- packages/object/tests/hashgraph.test.ts | 16 +++--- 2 files changed, 44 insertions(+), 46 deletions(-) diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index bde9a13d..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,39 +213,46 @@ export class HashGraph { return hash; } - depthFirstSearch( - origin: Hash, - subgraph: ObjectSet, - visited: Map = new Map(), - ): Hash[] { + kahnsAlgorithm(origin: Hash, subgraph: ObjectSet): Hash[] { const result: Hash[] = []; + const inDegree = new Map(); + const queue: Hash[] = []; + for (const hash of subgraph.entries()) { - visited.set(hash, DepthFirstSearchState.UNVISITED); + inDegree.set(hash, 0); } - const visit = (hash: Hash) => { - visited.set(hash, DepthFirstSearchState.VISITING); - const children = this.forwardEdges.get(hash) || []; + for (const [vertex, children] of this.forwardEdges) { + if (!inDegree.has(vertex)) continue; 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); - } + if (!inDegree.has(child)) continue; + inDegree.set(child, (inDegree.get(child) || 0) + 1); } + } - result.push(hash); - visited.set(hash, DepthFirstSearchState.VISITED); - }; + let head = 0; + queue.push(origin); + while (queue.length > 0) { + const current = queue[head]; + head++; + if (!current) continue; - visit(origin); + result.push(current); + + 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); + } + } + + if (head > queue.length / 2) { + queue.splice(0, head); + head = 0; + } + } return result; } @@ -262,8 +263,7 @@ export class HashGraph { origin: Hash = HashGraph.rootHash, subgraph: ObjectSet = new ObjectSet(this.vertices.keys()), ): Hash[] { - const result = this.depthFirstSearch(origin, subgraph); - result.reverse(); + const result = this.kahnsAlgorithm(origin, subgraph); if (!updateBitsets) return result; this.reachablePredecessors.clear(); this.topoSortedIndex.clear(); @@ -468,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; } diff --git a/packages/object/tests/hashgraph.test.ts b/packages/object/tests/hashgraph.test.ts index dc681947..f4b4dc09 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 }, ]);