diff --git a/packages/blueprints/src/AddWinsSetWithACL/index.ts b/packages/blueprints/src/AddWinsSetWithACL/index.ts index 23ff7151..8671a82a 100644 --- a/packages/blueprints/src/AddWinsSetWithACL/index.ts +++ b/packages/blueprints/src/AddWinsSetWithACL/index.ts @@ -23,10 +23,7 @@ export class AddWinsSetWithACL implements DRP { if (!this.state.get(value)) this.state.set(value, true); } - add(sender: string, value: T): void { - if (this.acl && !this.acl.isWriter(sender)) { - throw new Error("Only writers can add values."); - } + add(value: T): void { this._add(value); } @@ -34,10 +31,7 @@ export class AddWinsSetWithACL implements DRP { if (this.state.get(value)) this.state.set(value, false); } - remove(sender: string, value: T): void { - if (this.acl && !this.acl.isWriter(sender)) { - throw new Error("Only writers can remove values."); - } + remove(value: T): void { this._remove(value); } diff --git a/packages/object/src/index.ts b/packages/object/src/index.ts index d7f285d6..07b7689d 100644 --- a/packages/object/src/index.ts +++ b/packages/object/src/index.ts @@ -164,6 +164,11 @@ export class DRPObject implements IDRPObject { } try { + const drp = this._computeDRP(vertex.dependencies); + if (!this._checkWriterPermission(drp, vertex.peerId)) { + throw new Error(`${vertex.peerId} does not have write permission.`); + } + this.hashGraph.addVertex( vertex.operation, vertex.dependencies, @@ -172,7 +177,8 @@ export class DRPObject implements IDRPObject { vertex.signature, ); - this._setState(vertex); + this._applyOperation(drp, vertex.operation); + this._setState(vertex, this._getDRPState(drp)); } catch (e) { missing.push(vertex.hash); } @@ -196,6 +202,15 @@ export class DRPObject implements IDRPObject { } } + // check if the given peer has write permission + private _checkWriterPermission(drp: DRP, peerId: string): boolean { + if (drp.acl) { + return drp.acl.isWriter(peerId); + } + return true; + } + + // apply the operation to the DRP private _applyOperation(drp: DRP, operation: Operation) { const { type, value } = operation; @@ -218,11 +233,11 @@ export class DRPObject implements IDRPObject { target[methodName](...args); } - private _computeState( + // compute the DRP based on all dependencies of the current vertex using partial linearization + private _computeDRP( vertexDependencies: Hash[], - vertexOperation?: Operation | undefined, - // biome-ignore lint: values can be anything - ): Map { + vertexOperation?: Operation, + ): DRP { const subgraph: ObjectSet = new ObjectSet(); const lca = vertexDependencies.length === 1 @@ -256,27 +271,55 @@ export class DRPObject implements IDRPObject { this._applyOperation(drp, vertexOperation); } + return drp; + } + + // get the map representing the state of the given DRP by mapping variable names to their corresponding values + private _getDRPState( + drp: DRP, + // biome-ignore lint: values can be anything + ): DRPState { const varNames: string[] = Object.keys(drp); // biome-ignore lint: values can be anything - const newState: Map = new Map(); + const drpState: DRPState = { + state: new Map(), + }; for (const varName of varNames) { - newState.set(varName, drp[varName]); + drpState.state.set(varName, drp[varName]); } - return newState; + return drpState; } - private _setState(vertex: Vertex) { - const newState = this._computeState(vertex.dependencies, vertex.operation); - this.states.set(vertex.hash, { state: newState }); + // compute the DRP state based on all dependencies of the current vertex + private _computeDRPState( + vertexDependencies: Hash[], + vertexOperation?: Operation, + // biome-ignore lint: values can be anything + ): DRPState { + const drp = this._computeDRP(vertexDependencies, vertexOperation); + return this._getDRPState(drp); + } + + // store the state of the DRP corresponding to the given vertex + private _setState( + vertex: Vertex, + // biome-ignore lint: values can be anything + drpState?: DRPState, + ) { + this.states.set( + vertex.hash, + drpState ?? this._computeDRPState(vertex.dependencies, vertex.operation), + ); } + // update the DRP's attributes based on all the vertices in the hashgraph private _updateDRPState() { if (!this.drp) { return; } const currentDRP = this.drp as DRP; - const newState = this._computeState(this.hashGraph.getFrontier()); - for (const [key, value] of newState.entries()) { + const newState = this._computeDRPState(this.hashGraph.getFrontier()); + for (const [key, value] of newState.state.entries()) { if (key in currentDRP && typeof currentDRP[key] !== "function") { currentDRP[key] = value; } diff --git a/packages/object/tests/hashgraph.test.ts b/packages/object/tests/hashgraph.test.ts index dc681947..939659bf 100644 --- a/packages/object/tests/hashgraph.test.ts +++ b/packages/object/tests/hashgraph.test.ts @@ -675,7 +675,7 @@ describe("Operation with ACL tests", () => { drp1.acl.grant("peer1", "peer2", "publicKey2"); obj2.merge(obj1.hashGraph.getAllVertices()); - drp2.add("peer2", 1); + drp2.add(1); obj1.merge(obj2.hashGraph.getAllVertices()); expect(drp1.contains(1)).toBe(true); }); @@ -691,7 +691,7 @@ describe("Operation with ACL tests", () => { obj2.merge(obj1.hashGraph.getAllVertices()); expect(drp2.acl.isWriter("peer2")).toBe(true); - drp2.add("peer2", 1); + drp2.add(1); obj1.merge(obj2.hashGraph.getAllVertices()); drp1.acl.revoke("peer1", "peer2"); @@ -699,3 +699,95 @@ describe("Operation with ACL tests", () => { expect(drp2.acl.isWriter("peer2")).toBe(false); }); }); + +describe("Writer permission tests", () => { + let obj1: DRPObject; + let obj2: DRPObject; + let obj3: DRPObject; + + beforeEach(async () => { + const peerIdToPublicKeyMap = new Map([["peer1", "publicKey1"]]); + obj1 = new DRPObject("peer1", new AddWinsSetWithACL(peerIdToPublicKeyMap)); + obj2 = new DRPObject("peer2", new AddWinsSetWithACL(peerIdToPublicKeyMap)); + obj3 = new DRPObject("peer3", new AddWinsSetWithACL(peerIdToPublicKeyMap)); + }); + + test("Node without writer permission can generate vertex locally", () => { + const drp = obj1.drp as AddWinsSetWithACL; + drp.add(1); + drp.add(2); + + expect(drp.contains(1)).toBe(true); + expect(drp.contains(2)).toBe(true); + }); + + test("Discard vertex if creator does not have write permission", () => { + const drp1 = obj1.drp as AddWinsSetWithACL; + const drp2 = obj2.drp as AddWinsSetWithACL; + + drp1.add(1); + drp2.add(2); + + obj1.merge(obj2.hashGraph.getAllVertices()); + expect(drp1.contains(2)).toBe(false); + }); + + test("Accept vertex if creator has write permission", () => { + /* + ROOT -- V1:ADD(1) -- V2:GRANT(peer2) -- V3:ADD(4) + */ + const drp1 = obj1.drp as AddWinsSetWithACL; + const drp2 = obj2.drp as AddWinsSetWithACL; + + drp1.add(1); + drp1.acl.grant("peer1", "peer2", "publicKey2"); + expect(drp1.acl.isAdmin("peer1")).toBe(true); + + obj2.merge(obj1.hashGraph.getAllVertices()); + expect(drp2.contains(1)).toBe(true); + expect(drp2.acl.isWriter("peer2")).toBe(true); + + drp2.add(4); + obj1.merge(obj2.hashGraph.getAllVertices()); + expect(drp1.contains(4)).toBe(true); + }); + + test("Discard vertex if writer permission is revoked", () => { + /* + __ V4:ADD(1) -- + / \ + ROOT -- V1:GRANT(peer2) -- V2:grant(peer3) V6:REVOKE(peer3) -- V7:ADD(4) + \ / + -- V5:ADD(2) -- + */ + const drp1 = obj1.drp as AddWinsSetWithACL; + const drp2 = obj2.drp as AddWinsSetWithACL; + const drp3 = obj3.drp as AddWinsSetWithACL; + + drp1.acl.grant("peer1", "peer2", "publicKey2"); + drp1.acl.grant("peer1", "peer3", "publicKey3"); + obj2.merge(obj1.hashGraph.getAllVertices()); + obj3.merge(obj1.hashGraph.getAllVertices()); + + drp2.add(1); + drp3.add(2); + obj1.merge(obj2.hashGraph.getAllVertices()); + obj1.merge(obj3.hashGraph.getAllVertices()); + obj2.merge(obj3.hashGraph.getAllVertices()); + obj3.merge(obj2.hashGraph.getAllVertices()); + expect(drp1.contains(1)).toBe(true); + expect(drp1.contains(2)).toBe(true); + + drp1.acl.revoke("peer1", "peer3"); + obj3.merge(obj1.hashGraph.getAllVertices()); + drp3.add(3); + obj2.merge(obj3.hashGraph.getAllVertices()); + expect(drp2.contains(3)).toBe(false); + + drp2.add(4); + obj1.merge(obj2.hashGraph.getAllVertices()); + obj1.merge(obj3.hashGraph.getAllVertices()); + expect(drp1.contains(3)).toBe(false); + expect(drp1.contains(4)).toBe(true); + }); +});