diff --git a/packages/network/src/proto/drp/object/v1/object_pb.ts b/packages/network/src/proto/drp/object/v1/object_pb.ts index 3e229e0e..0b75129e 100644 --- a/packages/network/src/proto/drp/object/v1/object_pb.ts +++ b/packages/network/src/proto/drp/object/v1/object_pb.ts @@ -16,6 +16,7 @@ export interface Vertex { peerId: string; operation: Vertex_Operation | undefined; dependencies: string[]; + timestamp: number; signature: string; } @@ -32,7 +33,7 @@ export interface DRPObjectBase { } function createBaseVertex(): Vertex { - return { hash: "", peerId: "", operation: undefined, dependencies: [], signature: "" }; + return { hash: "", peerId: "", operation: undefined, dependencies: [], timestamp: 0, signature: "" }; } export const Vertex: MessageFns = { @@ -49,8 +50,11 @@ export const Vertex: MessageFns = { for (const v of message.dependencies) { writer.uint32(34).string(v!); } + if (message.timestamp !== 0) { + writer.uint32(40).int64(message.timestamp); + } if (message.signature !== "") { - writer.uint32(42).string(message.signature); + writer.uint32(50).string(message.signature); } return writer; }, @@ -95,7 +99,15 @@ export const Vertex: MessageFns = { continue; } case 5: { - if (tag !== 42) { + if (tag !== 40) { + break; + } + + message.timestamp = longToNumber(reader.int64()); + continue; + } + case 6: { + if (tag !== 50) { break; } @@ -119,6 +131,7 @@ export const Vertex: MessageFns = { dependencies: globalThis.Array.isArray(object?.dependencies) ? object.dependencies.map((e: any) => globalThis.String(e)) : [], + timestamp: isSet(object.timestamp) ? globalThis.Number(object.timestamp) : 0, signature: isSet(object.signature) ? globalThis.String(object.signature) : "", }; }, @@ -137,6 +150,9 @@ export const Vertex: MessageFns = { if (message.dependencies?.length) { obj.dependencies = message.dependencies; } + if (message.timestamp !== 0) { + obj.timestamp = Math.round(message.timestamp); + } if (message.signature !== "") { obj.signature = message.signature; } @@ -154,6 +170,7 @@ export const Vertex: MessageFns = { ? Vertex_Operation.fromPartial(object.operation) : undefined; message.dependencies = object.dependencies?.map((e) => e) || []; + message.timestamp = object.timestamp ?? 0; message.signature = object.signature ?? ""; return message; }, @@ -380,6 +397,17 @@ type KeysOfUnion = T extends T ? keyof T : never; export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; +function longToNumber(int64: { toString(): string }): number { + const num = globalThis.Number(int64.toString()); + if (num > globalThis.Number.MAX_SAFE_INTEGER) { + throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); + } + if (num < globalThis.Number.MIN_SAFE_INTEGER) { + throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); + } + return num; +} + function isSet(value: any): boolean { return value !== null && value !== undefined; } diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index de6fa2f8..fe591f23 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -238,6 +238,7 @@ export async function verifyIncomingVertices( value: vertex.operation?.value, }, dependencies: vertex.dependencies, + timestamp: vertex.timestamp, signature: vertex.signature, }; }); diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index af65bd61..bde9a13d 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -59,12 +59,13 @@ export class HashGraph { /* computeHash( "", - { type: OperationType.NOP }, + { type: OperationType.NOP, value: null }, [], - ) + -1, + ); */ static readonly rootHash: Hash = - "a65c9cbd875fd3d602adb69a90adb98c4e2c3f26bdf3a2bf597f3548971f2c93"; + "425d2b1f5243dbf23c685078034b06fbfa71dc31dcce30f614e28023f140ff13"; private arePredecessorsFresh = false; private reachablePredecessors: Map = new Map(); private topoSortedIndex: Map = new Map(); @@ -89,6 +90,7 @@ export class HashGraph { value: null, }, dependencies: [], + timestamp: -1, signature: "", }; this.vertices.set(HashGraph.rootHash, rootVertex); @@ -101,13 +103,15 @@ export class HashGraph { addToFrontier(operation: Operation): Vertex { const deps = this.getFrontier(); - const hash = computeHash(this.peerId, operation, deps); + const currentTimestamp = Date.now(); + const hash = computeHash(this.peerId, operation, deps, currentTimestamp); const vertex: Vertex = { hash, peerId: this.peerId, operation: operation ?? { type: OperationType.NOP }, dependencies: deps, + timestamp: currentTimestamp, signature: "", }; @@ -151,17 +155,29 @@ export class HashGraph { operation: Operation, deps: Hash[], peerId: string, + timestamp: number, signature: string, ): Hash { - const hash = computeHash(peerId, operation, deps); + const hash = computeHash(peerId, operation, deps, timestamp); if (this.vertices.has(hash)) { return hash; // Vertex already exists } - if ( - !deps.every((dep) => this.forwardEdges.has(dep) || this.vertices.has(dep)) - ) { - throw new Error("Invalid dependency detected."); + for (const dep of deps) { + const vertex = this.vertices.get(dep); + if (vertex === undefined) { + throw new Error("Invalid dependency detected."); + } + if (vertex.timestamp > 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 (timestamp > currentTimestamp) { + // Vertex created in the future is invalid + throw new Error("Invalid timestamp detected."); } const vertex: Vertex = { @@ -169,6 +185,7 @@ export class HashGraph { peerId, operation, dependencies: deps, + timestamp, signature, }; this.vertices.set(hash, vertex); @@ -502,12 +519,13 @@ export class HashGraph { } } -function computeHash( +function computeHash( peerId: string, operation: Operation, deps: Hash[], + timestamp: number, ): Hash { - const serialized = JSON.stringify({ operation, deps, peerId }); + const serialized = JSON.stringify({ operation, deps, peerId, timestamp }); const hash = crypto.createHash("sha256").update(serialized).digest("hex"); return hash; } diff --git a/packages/object/src/index.ts b/packages/object/src/index.ts index 08c75010..7f57948d 100644 --- a/packages/object/src/index.ts +++ b/packages/object/src/index.ts @@ -133,6 +133,7 @@ export class DRPObject implements IDRPObject { peerId: vertex.peerId, operation: vertex.operation, dependencies: vertex.dependencies, + timestamp: vertex.timestamp, }); this.vertices.push(serializedVertex); this._notify("callFn", [serializedVertex]); @@ -155,6 +156,7 @@ export class DRPObject implements IDRPObject { vertex.operation, vertex.dependencies, vertex.peerId, + vertex.timestamp, vertex.signature, ); diff --git a/packages/object/src/proto/drp/object/v1/object.proto b/packages/object/src/proto/drp/object/v1/object.proto index 9af95a91..3ee17d5e 100644 --- a/packages/object/src/proto/drp/object/v1/object.proto +++ b/packages/object/src/proto/drp/object/v1/object.proto @@ -14,7 +14,8 @@ message Vertex { string peer_id = 2; Operation operation = 3; repeated string dependencies = 4; - string signature = 5; + int64 timestamp = 5; + string signature = 6; } message DRPObjectBase { diff --git a/packages/object/src/proto/drp/object/v1/object_pb.ts b/packages/object/src/proto/drp/object/v1/object_pb.ts index 3e229e0e..0b75129e 100644 --- a/packages/object/src/proto/drp/object/v1/object_pb.ts +++ b/packages/object/src/proto/drp/object/v1/object_pb.ts @@ -16,6 +16,7 @@ export interface Vertex { peerId: string; operation: Vertex_Operation | undefined; dependencies: string[]; + timestamp: number; signature: string; } @@ -32,7 +33,7 @@ export interface DRPObjectBase { } function createBaseVertex(): Vertex { - return { hash: "", peerId: "", operation: undefined, dependencies: [], signature: "" }; + return { hash: "", peerId: "", operation: undefined, dependencies: [], timestamp: 0, signature: "" }; } export const Vertex: MessageFns = { @@ -49,8 +50,11 @@ export const Vertex: MessageFns = { for (const v of message.dependencies) { writer.uint32(34).string(v!); } + if (message.timestamp !== 0) { + writer.uint32(40).int64(message.timestamp); + } if (message.signature !== "") { - writer.uint32(42).string(message.signature); + writer.uint32(50).string(message.signature); } return writer; }, @@ -95,7 +99,15 @@ export const Vertex: MessageFns = { continue; } case 5: { - if (tag !== 42) { + if (tag !== 40) { + break; + } + + message.timestamp = longToNumber(reader.int64()); + continue; + } + case 6: { + if (tag !== 50) { break; } @@ -119,6 +131,7 @@ export const Vertex: MessageFns = { dependencies: globalThis.Array.isArray(object?.dependencies) ? object.dependencies.map((e: any) => globalThis.String(e)) : [], + timestamp: isSet(object.timestamp) ? globalThis.Number(object.timestamp) : 0, signature: isSet(object.signature) ? globalThis.String(object.signature) : "", }; }, @@ -137,6 +150,9 @@ export const Vertex: MessageFns = { if (message.dependencies?.length) { obj.dependencies = message.dependencies; } + if (message.timestamp !== 0) { + obj.timestamp = Math.round(message.timestamp); + } if (message.signature !== "") { obj.signature = message.signature; } @@ -154,6 +170,7 @@ export const Vertex: MessageFns = { ? Vertex_Operation.fromPartial(object.operation) : undefined; message.dependencies = object.dependencies?.map((e) => e) || []; + message.timestamp = object.timestamp ?? 0; message.signature = object.signature ?? ""; return message; }, @@ -380,6 +397,17 @@ type KeysOfUnion = T extends T ? keyof T : never; export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; +function longToNumber(int64: { toString(): string }): number { + const num = globalThis.Number(int64.toString()); + if (num > globalThis.Number.MAX_SAFE_INTEGER) { + throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); + } + if (num < globalThis.Number.MIN_SAFE_INTEGER) { + throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); + } + return num; +} + function isSet(value: any): boolean { return value !== null && value !== undefined; } diff --git a/packages/object/tests/hashgraph.test.ts b/packages/object/tests/hashgraph.test.ts index bcf17c81..19706c74 100644 --- a/packages/object/tests/hashgraph.test.ts +++ b/packages/object/tests/hashgraph.test.ts @@ -69,6 +69,7 @@ describe("HashGraph construction tests", () => { }, [], "", + Date.now(), "", ); obj1.hashGraph.addVertex( @@ -78,6 +79,7 @@ describe("HashGraph construction tests", () => { }, [hash], "", + Date.now(), "", ); expect(obj1.hashGraph.selfCheckConstraints()).toBe(false); @@ -388,54 +390,6 @@ describe("HashGraph for AddWinSet tests", () => { }); }); -describe("HashGraph for PseudoRandomWinsSet tests", () => { - let obj1: DRPObject; - let obj2: DRPObject; - let obj3: DRPObject; - let obj4: DRPObject; - let obj5: DRPObject; - - beforeEach(async () => { - obj1 = new DRPObject("peer1", new PseudoRandomWinsSet()); - obj2 = new DRPObject("peer2", new PseudoRandomWinsSet()); - obj3 = new DRPObject("peer3", new PseudoRandomWinsSet()); - obj4 = new DRPObject("peer4", new PseudoRandomWinsSet()); - obj5 = new DRPObject("peer5", new PseudoRandomWinsSet()); - }); - - test("Test: Many concurrent operations", () => { - /* - /-- V1:ADD(1) - /--- V2:ADD(2) - ROOT -- V3:ADD(3) - \__ V4:ADD(4) - \__ V5:ADD(5) - */ - - const drp1 = obj1.drp as PseudoRandomWinsSet; - const drp2 = obj2.drp as PseudoRandomWinsSet; - const drp3 = obj3.drp as PseudoRandomWinsSet; - const drp4 = obj4.drp as PseudoRandomWinsSet; - const drp5 = obj5.drp as PseudoRandomWinsSet; - - drp1.add(1); - drp2.add(2); - drp3.add(3); - drp4.add(4); - drp5.add(5); - - obj2.merge(obj1.hashGraph.getAllVertices()); - obj3.merge(obj2.hashGraph.getAllVertices()); - obj4.merge(obj3.hashGraph.getAllVertices()); - obj5.merge(obj4.hashGraph.getAllVertices()); - obj1.merge(obj5.hashGraph.getAllVertices()); - - const linearOps = obj1.hashGraph.linearizeOperations(); - // Pseudo-randomly chosen operation - expect(linearOps).toEqual([{ type: "add", value: 5 }]); - }); -}); - describe("HashGraph for undefined operations tests", () => { let obj1: DRPObject; let obj2: DRPObject; @@ -614,3 +568,68 @@ describe("Vertex state tests", () => { expect(drpStateV8?.state.get("state").get(3)).toBe(undefined); }); }); + +describe("Vertex timestamp tests", () => { + let obj1: DRPObject; + let obj2: DRPObject; + let obj3: DRPObject; + + beforeEach(async () => { + obj1 = new DRPObject("peer1", new AddWinsSet()); + obj2 = new DRPObject("peer1", new AddWinsSet()); + obj3 = new DRPObject("peer1", new AddWinsSet()); + }); + + test("Test: Vertex created in the future is invalid", () => { + const drp1 = obj1.drp as AddWinsSet; + + drp1.add(1); + + expect(() => + obj1.hashGraph.addVertex( + { + type: "add", + value: 1, + }, + obj1.hashGraph.getFrontier(), + "", + Number.POSITIVE_INFINITY, + "", + ), + ).toThrowError("Invalid timestamp detected."); + }); + + test("Test: Vertex's timestamp must not be less than any of its dependencies' timestamps", () => { + /* + __ V1:ADD(1) __ + / \ + ROOT -- V2:ADD(2) ---- V4:ADD(4) (invalid) + \ / + -- V3:ADD(3) -- + */ + + const drp1 = obj1.drp as AddWinsSet; + const drp2 = obj2.drp as AddWinsSet; + const drp3 = obj2.drp as AddWinsSet; + + drp1.add(1); + drp2.add(2); + drp3.add(3); + + obj1.merge(obj2.hashGraph.getAllVertices()); + obj1.merge(obj3.hashGraph.getAllVertices()); + + expect(() => + obj1.hashGraph.addVertex( + { + type: "add", + value: 1, + }, + obj1.hashGraph.getFrontier(), + "", + 1, + "", + ), + ).toThrowError("Invalid timestamp detected."); + }); +});