From 9e0c5d9e7eb74968f3cd6ab60f0016406c0cd2a7 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:53:33 +0700 Subject: [PATCH 01/20] initial timestamp commit --- .../src/proto/drp/object/v1/object_pb.ts | 30 ++++++++++++++++++- packages/object/src/hashgraph/index.ts | 3 +- packages/object/src/index.ts | 1 + .../src/proto/drp/object/v1/object.proto | 1 + .../src/proto/drp/object/v1/object_pb.ts | 30 ++++++++++++++++++- 5 files changed, 62 insertions(+), 3 deletions(-) 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 4db66f75..1680f04f 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 { nodeId: string; operation: Vertex_Operation | undefined; dependencies: string[]; + timestamp: number; } export interface Vertex_Operation { @@ -31,7 +32,7 @@ export interface DRPObjectBase { } function createBaseVertex(): Vertex { - return { hash: "", nodeId: "", operation: undefined, dependencies: [] }; + return { hash: "", nodeId: "", operation: undefined, dependencies: [], timestamp: 0 }; } export const Vertex: MessageFns = { @@ -48,6 +49,9 @@ 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); + } return writer; }, @@ -90,6 +94,14 @@ export const Vertex: MessageFns = { message.dependencies.push(reader.string()); continue; } + case 5: { + if (tag !== 40) { + break; + } + + message.timestamp = longToNumber(reader.int64()); + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; @@ -107,6 +119,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, }; }, @@ -124,6 +137,9 @@ export const Vertex: MessageFns = { if (message.dependencies?.length) { obj.dependencies = message.dependencies; } + if (message.timestamp !== 0) { + obj.timestamp = Math.round(message.timestamp); + } return obj; }, @@ -138,6 +154,7 @@ export const Vertex: MessageFns = { ? Vertex_Operation.fromPartial(object.operation) : undefined; message.dependencies = object.dependencies?.map((e) => e) || []; + message.timestamp = object.timestamp ?? 0; return message; }, }; @@ -363,6 +380,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/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index 1f3fbc7f..a521a554 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -145,7 +145,7 @@ export class HashGraph { * 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. */ - addVertex(operation: Operation, deps: Hash[], nodeId: string): Hash { + addVertex(operation: Operation, deps: Hash[], nodeId: string, timestamp?: number): Hash { const hash = computeHash(nodeId, operation, deps); if (this.vertices.has(hash)) { return hash; // Vertex already exists @@ -162,6 +162,7 @@ export class HashGraph { nodeId, operation, dependencies: deps, + timestamp: timestamp ?? Date.now(), }; this.vertices.set(hash, vertex); this.frontier.push(hash); diff --git a/packages/object/src/index.ts b/packages/object/src/index.ts index e5d69a61..12d3951d 100644 --- a/packages/object/src/index.ts +++ b/packages/object/src/index.ts @@ -148,6 +148,7 @@ export class DRPObject implements IDRPObject { vertex.operation, vertex.dependencies, vertex.nodeId, + vertex.timestamp, ); this._setState(vertex); diff --git a/packages/object/src/proto/drp/object/v1/object.proto b/packages/object/src/proto/drp/object/v1/object.proto index 5dd8d9c9..7d2f4362 100644 --- a/packages/object/src/proto/drp/object/v1/object.proto +++ b/packages/object/src/proto/drp/object/v1/object.proto @@ -14,6 +14,7 @@ message Vertex { string node_id = 2; Operation operation = 3; repeated string dependencies = 4; + int64 timestamp = 5; } 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 4db66f75..1680f04f 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 { nodeId: string; operation: Vertex_Operation | undefined; dependencies: string[]; + timestamp: number; } export interface Vertex_Operation { @@ -31,7 +32,7 @@ export interface DRPObjectBase { } function createBaseVertex(): Vertex { - return { hash: "", nodeId: "", operation: undefined, dependencies: [] }; + return { hash: "", nodeId: "", operation: undefined, dependencies: [], timestamp: 0 }; } export const Vertex: MessageFns = { @@ -48,6 +49,9 @@ 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); + } return writer; }, @@ -90,6 +94,14 @@ export const Vertex: MessageFns = { message.dependencies.push(reader.string()); continue; } + case 5: { + if (tag !== 40) { + break; + } + + message.timestamp = longToNumber(reader.int64()); + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; @@ -107,6 +119,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, }; }, @@ -124,6 +137,9 @@ export const Vertex: MessageFns = { if (message.dependencies?.length) { obj.dependencies = message.dependencies; } + if (message.timestamp !== 0) { + obj.timestamp = Math.round(message.timestamp); + } return obj; }, @@ -138,6 +154,7 @@ export const Vertex: MessageFns = { ? Vertex_Operation.fromPartial(object.operation) : undefined; message.dependencies = object.dependencies?.map((e) => e) || []; + message.timestamp = object.timestamp ?? 0; return message; }, }; @@ -363,6 +380,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; } From bbaf27d3490902d3693ea548f1ace1dd427021d5 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:07:41 +0700 Subject: [PATCH 02/20] fix: timestamp --- packages/object/src/hashgraph/index.ts | 2 ++ packages/object/src/index.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index a521a554..e61092b0 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -89,6 +89,7 @@ export class HashGraph { value: null, }, dependencies: [], + timestamp: 0, }; this.vertices.set(HashGraph.rootHash, rootVertex); this.frontier.push(HashGraph.rootHash); @@ -107,6 +108,7 @@ export class HashGraph { nodeId: this.nodeId, operation: operation ?? { type: OperationType.NOP }, dependencies: deps, + timestamp: Date.now(), }; this.vertices.set(hash, vertex); diff --git a/packages/object/src/index.ts b/packages/object/src/index.ts index 12d3951d..56887b69 100644 --- a/packages/object/src/index.ts +++ b/packages/object/src/index.ts @@ -126,6 +126,7 @@ export class DRPObject implements IDRPObject { nodeId: vertex.nodeId, operation: vertex.operation, dependencies: vertex.dependencies, + timestamp: vertex.timestamp, }); this.vertices.push(serializedVertex); this._notify("callFn", [serializedVertex]); From b7ab721fb10add9709205da18b6edb30405584a6 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:56:32 +0700 Subject: [PATCH 03/20] fix stuff --- packages/node/src/handlers.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 615c720a..f1b3d6b9 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -72,6 +72,7 @@ async function updateHandler(node: DRPNode, data: Uint8Array, sender: string) { value: v.operation?.value, }, dependencies: v.dependencies, + timestamp: v.timestamp, }; }), ); @@ -147,6 +148,7 @@ function syncAcceptHandler(node: DRPNode, sender: string, data: Uint8Array) { value: v.operation?.value, }, dependencies: v.dependencies, + timestamp: v.timestamp, }; }); From e254b625e44b279eb65ac4656ca7a5d228148d7d Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:58:17 +0700 Subject: [PATCH 04/20] lint fix --- packages/object/src/hashgraph/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index e61092b0..36fcfd35 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -147,7 +147,12 @@ export class HashGraph { * 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. */ - addVertex(operation: Operation, deps: Hash[], nodeId: string, timestamp?: number): Hash { + addVertex( + operation: Operation, + deps: Hash[], + nodeId: string, + timestamp?: number, + ): Hash { const hash = computeHash(nodeId, operation, deps); if (this.vertices.has(hash)) { return hash; // Vertex already exists From 934a11e5852887bb6b45f70dbfbe17bc142c520c Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Tue, 17 Dec 2024 14:03:02 +0700 Subject: [PATCH 05/20] add timestamp check to the UPDATE handler --- packages/node/src/handlers.ts | 30 +++++++++++++++++------------- packages/object/src/index.ts | 3 +++ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index f1b3d6b9..6cdf8a3d 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -62,20 +62,11 @@ async function updateHandler(node: DRPNode, data: Uint8Array, sender: string) { return false; } - const [merged, _] = object.merge( - updateMessage.vertices.map((v) => { - return { - hash: v.hash, - nodeId: v.nodeId, - operation: { - type: v.operation?.type ?? "", - value: v.operation?.value, - }, - dependencies: v.dependencies, - timestamp: v.timestamp, - }; - }), + const verifiedVertices = verifyIncomingVertices( + object, + updateMessage.vertices, ); + const [merged, _] = object.merge(verifiedVertices); if (!merged) { await node.syncObject(updateMessage.objectId, sender); @@ -220,3 +211,16 @@ export function drpObjectChangesHandler( log.error("::createObject: Invalid origin function"); } } + +export function verifyIncomingVertices( + object: DRPObject, + incomingVertices: ObjectPb.Vertex[], +): Vertex[] { + const currentTimestamp = Date.now(); + + return incomingVertices.filter( + (vertex) => + vertex.timestamp <= currentTimestamp && + currentTimestamp - vertex.timestamp < object.vertexExpirationPeriod, + ); +} diff --git a/packages/object/src/index.ts b/packages/object/src/index.ts index 56887b69..95582591 100644 --- a/packages/object/src/index.ts +++ b/packages/object/src/index.ts @@ -42,6 +42,7 @@ export interface IDRPObject extends ObjectPb.DRPObjectBase { // snake_casing to match the JSON config export interface DRPObjectConfig { log_config?: LoggerOptions; + vertex_expiration_period?: number; } export let log: Logger; @@ -58,6 +59,7 @@ export class DRPObject implements IDRPObject { states: Map; originalDRP: DRP; subscriptions: DRPObjectCallback[]; + vertexExpirationPeriod: number; constructor( nodeId: string, @@ -92,6 +94,7 @@ export class DRPObject implements IDRPObject { Object.getOwnPropertyDescriptors(structuredClone(drp)), ); this.vertices = this.hashGraph.getAllVertices(); + this.vertexExpirationPeriod = config?.vertex_expiration_period ?? Number.POSITIVE_INFINITY; } // This function is black magic, it allows us to intercept calls to the DRP object From 5b477ed127dcf799144d308fb72a16d36b508b78 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:44:19 +0700 Subject: [PATCH 06/20] add filter to SYNC handler --- packages/node/src/handlers.ts | 35 ++++++++++++++++++++--------------- packages/object/src/index.ts | 3 ++- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 6cdf8a3d..0c7176ef 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -62,7 +62,7 @@ async function updateHandler(node: DRPNode, data: Uint8Array, sender: string) { return false; } - const verifiedVertices = verifyIncomingVertices( + const verifiedVertices = cleanIncomingVertices( object, updateMessage.vertices, ); @@ -130,18 +130,10 @@ function syncAcceptHandler(node: DRPNode, sender: string, data: Uint8Array) { return; } - const vertices: Vertex[] = syncAcceptMessage.requested.map((v) => { - return { - hash: v.hash, - nodeId: v.nodeId, - operation: { - type: v.operation?.type ?? "", - value: v.operation?.value, - }, - dependencies: v.dependencies, - timestamp: v.timestamp, - }; - }); + const vertices: Vertex[] = cleanIncomingVertices( + object, + syncAcceptMessage.requested, + ); if (vertices.length !== 0) { object.merge(vertices); @@ -212,13 +204,26 @@ export function drpObjectChangesHandler( } } -export function verifyIncomingVertices( +export function cleanIncomingVertices( object: DRPObject, incomingVertices: ObjectPb.Vertex[], ): Vertex[] { const currentTimestamp = Date.now(); - return incomingVertices.filter( + const vertices: Vertex[] = incomingVertices.map((v) => { + return { + hash: v.hash, + nodeId: v.nodeId, + operation: { + type: v.operation?.type ?? "", + value: v.operation?.value, + }, + dependencies: v.dependencies, + timestamp: v.timestamp, + }; + }); + + return vertices.filter( (vertex) => vertex.timestamp <= currentTimestamp && currentTimestamp - vertex.timestamp < object.vertexExpirationPeriod, diff --git a/packages/object/src/index.ts b/packages/object/src/index.ts index 95582591..3fe12140 100644 --- a/packages/object/src/index.ts +++ b/packages/object/src/index.ts @@ -94,7 +94,8 @@ export class DRPObject implements IDRPObject { Object.getOwnPropertyDescriptors(structuredClone(drp)), ); this.vertices = this.hashGraph.getAllVertices(); - this.vertexExpirationPeriod = config?.vertex_expiration_period ?? Number.POSITIVE_INFINITY; + this.vertexExpirationPeriod = + config?.vertex_expiration_period ?? Number.POSITIVE_INFINITY; } // This function is black magic, it allows us to intercept calls to the DRP object From d78e760db936626afeb65f1cf5b7501f29a92868 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:56:47 +0700 Subject: [PATCH 07/20] add config to the param list --- packages/node/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 2a67f695..3bba3486 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -6,7 +6,7 @@ import { type DRPNetworkNodeConfig, NetworkPb, } from "@ts-drp/network"; -import { type DRP, DRPObject } from "@ts-drp/object"; +import { type DRP, DRPObject, type DRPObjectConfig } from "@ts-drp/object"; import { drpMessagesHandler } from "./handlers.js"; import * as operations from "./operations.js"; import { DRPObjectStore } from "./store/index.js"; @@ -77,8 +77,9 @@ export class DRPNode { abi?: string, sync?: boolean, peerId?: string, + config?: DRPObjectConfig, ) { - const object = new DRPObject(this.networkNode.peerId, drp, id, abi); + const object = new DRPObject(this.networkNode.peerId, drp, id, abi, config); operations.createObject(this, object); operations.subscribeObject(this, object.id); if (sync) { From 02d8214472891f32ca90ca009864a85026e31292 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Fri, 20 Dec 2024 04:42:45 +0700 Subject: [PATCH 08/20] add checks for timestamp order, disable timeout period --- packages/node/src/handlers.ts | 8 +------- packages/object/src/hashgraph/index.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 0c7176ef..bd960664 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -208,8 +208,6 @@ export function cleanIncomingVertices( object: DRPObject, incomingVertices: ObjectPb.Vertex[], ): Vertex[] { - const currentTimestamp = Date.now(); - const vertices: Vertex[] = incomingVertices.map((v) => { return { hash: v.hash, @@ -223,9 +221,5 @@ export function cleanIncomingVertices( }; }); - return vertices.filter( - (vertex) => - vertex.timestamp <= currentTimestamp && - currentTimestamp - vertex.timestamp < object.vertexExpirationPeriod, - ); + return vertices; } diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index 36fcfd35..106020ed 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -151,7 +151,7 @@ export class HashGraph { operation: Operation, deps: Hash[], nodeId: string, - timestamp?: number, + timestamp: number, ): Hash { const hash = computeHash(nodeId, operation, deps); if (this.vertices.has(hash)) { @@ -159,17 +159,22 @@ export class HashGraph { } if ( - !deps.every((dep) => this.forwardEdges.has(dep) || this.vertices.has(dep)) + !deps.every((dep) => this.vertices.has(dep)) ) { throw new Error("Invalid dependency detected."); } + const currentTimestamp = Date.now(); + if (timestamp > currentTimestamp || !deps.every((dep) => this.vertices.get(dep)?.timestamp <= timestamp)) { + throw new Error("Invalid timestamp detected."); + } + const vertex: Vertex = { hash, nodeId, operation, dependencies: deps, - timestamp: timestamp ?? Date.now(), + timestamp, }; this.vertices.set(hash, vertex); this.frontier.push(hash); From 6192670617d55aac18aca6f3da84260f6dde91c5 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:44:21 +0700 Subject: [PATCH 09/20] fix unit tests --- packages/object/tests/hashgraph.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/object/tests/hashgraph.test.ts b/packages/object/tests/hashgraph.test.ts index 5213d82d..a906b9ab 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( { @@ -77,6 +78,7 @@ describe("HashGraph construction tests", () => { }, [hash], "", + Date.now(), ); expect(obj1.hashGraph.selfCheckConstraints()).toBe(false); From 4b6049df02532068dbd8f85bd35a610ae02c9058 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:49:23 +0700 Subject: [PATCH 10/20] discard changes, fix lint --- packages/node/src/handlers.ts | 51 ++++++++++++-------------- packages/object/src/hashgraph/index.ts | 9 +++-- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index bd960664..615c720a 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -62,11 +62,19 @@ async function updateHandler(node: DRPNode, data: Uint8Array, sender: string) { return false; } - const verifiedVertices = cleanIncomingVertices( - object, - updateMessage.vertices, + const [merged, _] = object.merge( + updateMessage.vertices.map((v) => { + return { + hash: v.hash, + nodeId: v.nodeId, + operation: { + type: v.operation?.type ?? "", + value: v.operation?.value, + }, + dependencies: v.dependencies, + }; + }), ); - const [merged, _] = object.merge(verifiedVertices); if (!merged) { await node.syncObject(updateMessage.objectId, sender); @@ -130,10 +138,17 @@ function syncAcceptHandler(node: DRPNode, sender: string, data: Uint8Array) { return; } - const vertices: Vertex[] = cleanIncomingVertices( - object, - syncAcceptMessage.requested, - ); + const vertices: Vertex[] = syncAcceptMessage.requested.map((v) => { + return { + hash: v.hash, + nodeId: v.nodeId, + operation: { + type: v.operation?.type ?? "", + value: v.operation?.value, + }, + dependencies: v.dependencies, + }; + }); if (vertices.length !== 0) { object.merge(vertices); @@ -203,23 +218,3 @@ export function drpObjectChangesHandler( log.error("::createObject: Invalid origin function"); } } - -export function cleanIncomingVertices( - object: DRPObject, - incomingVertices: ObjectPb.Vertex[], -): Vertex[] { - const vertices: Vertex[] = incomingVertices.map((v) => { - return { - hash: v.hash, - nodeId: v.nodeId, - operation: { - type: v.operation?.type ?? "", - value: v.operation?.value, - }, - dependencies: v.dependencies, - timestamp: v.timestamp, - }; - }); - - return vertices; -} diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index 106020ed..b81108e7 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -158,14 +158,15 @@ export class HashGraph { return hash; // Vertex already exists } - if ( - !deps.every((dep) => this.vertices.has(dep)) - ) { + if (!deps.every((dep) => this.vertices.has(dep))) { throw new Error("Invalid dependency detected."); } const currentTimestamp = Date.now(); - if (timestamp > currentTimestamp || !deps.every((dep) => this.vertices.get(dep)?.timestamp <= timestamp)) { + if ( + timestamp > currentTimestamp || + !deps.every((dep) => this.vertices.get(dep)?.timestamp <= timestamp) + ) { throw new Error("Invalid timestamp detected."); } From e047b09c63e34b523617947ed70d249fb3fe832a Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:08:05 +0700 Subject: [PATCH 11/20] fix timestamp checks --- packages/node/src/handlers.ts | 2 ++ packages/object/src/hashgraph/index.ts | 17 +++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 615c720a..f1b3d6b9 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -72,6 +72,7 @@ async function updateHandler(node: DRPNode, data: Uint8Array, sender: string) { value: v.operation?.value, }, dependencies: v.dependencies, + timestamp: v.timestamp, }; }), ); @@ -147,6 +148,7 @@ function syncAcceptHandler(node: DRPNode, sender: string, data: Uint8Array) { value: v.operation?.value, }, dependencies: v.dependencies, + timestamp: v.timestamp, }; }); diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index b81108e7..bfa329cf 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -158,15 +158,20 @@ export class HashGraph { return hash; // Vertex already exists } - if (!deps.every((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 its dependencies + throw new Error("Invalid timestamp detected."); + } } const currentTimestamp = Date.now(); - if ( - timestamp > currentTimestamp || - !deps.every((dep) => this.vertices.get(dep)?.timestamp <= timestamp) - ) { + if (timestamp > currentTimestamp) { + // Vertex created in the future is invalid throw new Error("Invalid timestamp detected."); } From 2f9a6c0c17ec1e3d5a93ba4f1251b4f7fb269f7b Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:11:10 +0700 Subject: [PATCH 12/20] remote deltaT --- packages/object/src/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/object/src/index.ts b/packages/object/src/index.ts index 3fe12140..56887b69 100644 --- a/packages/object/src/index.ts +++ b/packages/object/src/index.ts @@ -42,7 +42,6 @@ export interface IDRPObject extends ObjectPb.DRPObjectBase { // snake_casing to match the JSON config export interface DRPObjectConfig { log_config?: LoggerOptions; - vertex_expiration_period?: number; } export let log: Logger; @@ -59,7 +58,6 @@ export class DRPObject implements IDRPObject { states: Map; originalDRP: DRP; subscriptions: DRPObjectCallback[]; - vertexExpirationPeriod: number; constructor( nodeId: string, @@ -94,8 +92,6 @@ export class DRPObject implements IDRPObject { Object.getOwnPropertyDescriptors(structuredClone(drp)), ); this.vertices = this.hashGraph.getAllVertices(); - this.vertexExpirationPeriod = - config?.vertex_expiration_period ?? Number.POSITIVE_INFINITY; } // This function is black magic, it allows us to intercept calls to the DRP object From 0c1d5730a6107b0b0980a8262829abc9d16cd256 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:50:38 +0700 Subject: [PATCH 13/20] add unit tests --- packages/object/src/hashgraph/index.ts | 2 +- packages/object/tests/hashgraph.test.ts | 63 +++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index bfa329cf..45518b07 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -164,7 +164,7 @@ export class HashGraph { throw new Error("Invalid dependency detected."); } if (vertex.timestamp > timestamp) { - // Vertex's timestamp must not be less than its dependencies + // Vertex's timestamp must not be less than any of its dependencies' timestamps throw new Error("Invalid timestamp detected."); } } diff --git a/packages/object/tests/hashgraph.test.ts b/packages/object/tests/hashgraph.test.ts index a906b9ab..9f2fa9d6 100644 --- a/packages/object/tests/hashgraph.test.ts +++ b/packages/object/tests/hashgraph.test.ts @@ -619,3 +619,66 @@ 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) ---- V1: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."); + }); +}); From 80eeacba8b2a1c1947cf1b03e22cccf44318e447 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:55:05 +0700 Subject: [PATCH 14/20] prune config arg --- packages/node/src/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 3bba3486..2a67f695 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -6,7 +6,7 @@ import { type DRPNetworkNodeConfig, NetworkPb, } from "@ts-drp/network"; -import { type DRP, DRPObject, type DRPObjectConfig } from "@ts-drp/object"; +import { type DRP, DRPObject } from "@ts-drp/object"; import { drpMessagesHandler } from "./handlers.js"; import * as operations from "./operations.js"; import { DRPObjectStore } from "./store/index.js"; @@ -77,9 +77,8 @@ export class DRPNode { abi?: string, sync?: boolean, peerId?: string, - config?: DRPObjectConfig, ) { - const object = new DRPObject(this.networkNode.peerId, drp, id, abi, config); + const object = new DRPObject(this.networkNode.peerId, drp, id, abi); operations.createObject(this, object); operations.subscribeObject(this, object.id); if (sync) { From 828a729010c69126f71ff18ea3d1765bca3c41a1 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Sat, 21 Dec 2024 00:57:24 +0700 Subject: [PATCH 15/20] add timestamp to hash computation --- packages/object/src/hashgraph/index.ts | 12 +++++++----- packages/object/tests/hashgraph.test.ts | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index 45518b07..15c780f8 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -101,14 +101,15 @@ export class HashGraph { addToFrontier(operation: Operation): Vertex { const deps = this.getFrontier(); - const hash = computeHash(this.nodeId, operation, deps); + const currentTimestamp = Date.now(); + const hash = computeHash(this.nodeId, operation, deps, currentTimestamp); const vertex: Vertex = { hash, nodeId: this.nodeId, operation: operation ?? { type: OperationType.NOP }, dependencies: deps, - timestamp: Date.now(), + timestamp: currentTimestamp, }; this.vertices.set(hash, vertex); @@ -153,7 +154,7 @@ export class HashGraph { nodeId: string, timestamp: number, ): Hash { - const hash = computeHash(nodeId, operation, deps); + const hash = computeHash(nodeId, operation, deps, timestamp); if (this.vertices.has(hash)) { return hash; // Vertex already exists } @@ -513,12 +514,13 @@ export class HashGraph { } } -function computeHash( +function computeHash( nodeId: string, operation: Operation, deps: Hash[], + timestamp: number, ): Hash { - const serialized = JSON.stringify({ operation, deps, nodeId }); + const serialized = JSON.stringify({ operation, deps, nodeId, timestamp }); const hash = crypto.createHash("sha256").update(serialized).digest("hex"); return hash; } diff --git a/packages/object/tests/hashgraph.test.ts b/packages/object/tests/hashgraph.test.ts index 9f2fa9d6..1cf8b5ee 100644 --- a/packages/object/tests/hashgraph.test.ts +++ b/packages/object/tests/hashgraph.test.ts @@ -653,7 +653,7 @@ describe("Vertex timestamp tests", () => { /* __ V1:ADD(1) __ / \ - ROOT -- V2:ADD(2) ---- V1:ADD(4) (invalid) + ROOT -- V2:ADD(2) ---- V4:ADD(4) (invalid) \ / -- V3:ADD(3) -- */ From 735f6ebac14e426cfec36b2a11ce42fd7f8bb234 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 23 Dec 2024 04:37:44 +0700 Subject: [PATCH 16/20] fix lint --- packages/object/src/hashgraph/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index cb7e1de0..e70a48d2 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -156,7 +156,7 @@ export class HashGraph { nodeId: string, timestamp: number, signature: string, - ): Hash { + ): Hash { const hash = computeHash(nodeId, operation, deps, timestamp); if (this.vertices.has(hash)) { return hash; // Vertex already exists From 4935520e13e29bd3ef0f3aea9bbaef483dab3958 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:33:32 +0700 Subject: [PATCH 17/20] fix hashgraph test --- packages/object/tests/hashgraph.test.ts | 68 +++++-------------------- 1 file changed, 12 insertions(+), 56 deletions(-) diff --git a/packages/object/tests/hashgraph.test.ts b/packages/object/tests/hashgraph.test.ts index 76969442..e71645b0 100644 --- a/packages/object/tests/hashgraph.test.ts +++ b/packages/object/tests/hashgraph.test.ts @@ -390,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: 3 }]); - }); -}); - describe("HashGraph for undefined operations tests", () => { let obj1: DRPObject; let obj2: DRPObject; @@ -539,17 +491,18 @@ describe("Vertex state tests", () => { drp1.add(4); drp3.add(5); + expect(obj1.hashGraph.getFrontier().length).toBe(1); + expect(obj3.hashGraph.getFrontier().length).toBe(1); + const hashA4 = obj1.hashGraph.getFrontier()[0]; + const hashC5 = obj3.hashGraph.getFrontier()[0]; + obj1.merge(obj3.hashGraph.getAllVertices()); obj3.merge(obj1.hashGraph.getAllVertices()); drp1.add(6); - const hashA4 = - "8e6f4369010528ae3668efce452da04d077e0957955d62d671b90f2934c755fe"; - const hashC5 = - "a8d94f7e2b421be2d5cd1124ca9ddb831e38246065db6e9a32ce493ca9604038"; - const hashA6 = - "cd6a955f0734a09df1bff44c5e0458365d3a26ec7f1cae0df2c0f708b9f100a8"; + expect(obj1.hashGraph.getFrontier().length).toBe(1); + const hashA6 = obj1.hashGraph.getFrontier()[0]; const drpState1 = obj1.states.get(hashA4); expect(drpState1?.state.get("state").get(1)).toBe(true); @@ -606,6 +559,9 @@ describe("Vertex state tests", () => { drp1.remove(3); drp2.remove(1); + expect(obj1.hashGraph.getFrontier().length).toBe(1); + const hashV8 = obj1.hashGraph.getFrontier()[0]; + obj1.merge(obj2.hashGraph.getAllVertices()); obj1.merge(obj3.hashGraph.getAllVertices()); obj2.merge(obj1.hashGraph.getAllVertices()); @@ -613,8 +569,6 @@ describe("Vertex state tests", () => { obj3.merge(obj1.hashGraph.getAllVertices()); obj3.merge(obj2.hashGraph.getAllVertices()); - const hashV8 = - "be97d8fe9169800893c28b3d8aaefda517b98936efb069673e0250317b5e4a0b"; const drpStateV8 = obj1.states.get(hashV8); expect(drpStateV8?.state.get("state").get(1)).toBe(false); expect(drpStateV8?.state.get("state").get(2)).toBe(true); @@ -647,6 +601,7 @@ describe("Vertex timestamp tests", () => { obj1.hashGraph.getFrontier(), "", Number.POSITIVE_INFINITY, + "", ), ).toThrowError("Invalid timestamp detected."); }); @@ -680,6 +635,7 @@ describe("Vertex timestamp tests", () => { obj1.hashGraph.getFrontier(), "", 1, + "", ), ).toThrowError("Invalid timestamp detected."); }); From 0dfedb58ef9f844756b318fc9a17e7617698a1e1 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Tue, 24 Dec 2024 12:00:15 +0700 Subject: [PATCH 18/20] Change the timestamp for root vertex to -1 --- packages/object/src/hashgraph/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index e70a48d2..fc3f29cc 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -89,7 +89,7 @@ export class HashGraph { value: null, }, dependencies: [], - timestamp: 0, + timestamp: -1, signature: "", }; this.vertices.set(HashGraph.rootHash, rootVertex); From b6920c1ba76bbba0e22366b8abe81a76603be000 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Thu, 26 Dec 2024 12:04:00 +0700 Subject: [PATCH 19/20] fix rootHash --- packages/object/src/hashgraph/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index fc3f29cc..9c3545ec 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 = - "02465e287e3d086f12c6edd856953ca5ad0f01d6707bf8e410b4a601314c1ca5"; + "7ca74226e2670cd08cadd5ed351fb207ff97d505cf97051abd8c35278c93a7c8"; private arePredecessorsFresh = false; private reachablePredecessors: Map = new Map(); private topoSortedIndex: Map = new Map(); From 4df63d1f8889b6e4c3a536685e304d5eb8cf0720 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 30 Dec 2024 03:05:45 +0700 Subject: [PATCH 20/20] fix rootHash --- packages/object/src/hashgraph/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index 9bf999a6..bde9a13d 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -65,7 +65,7 @@ export class HashGraph { ); */ static readonly rootHash: Hash = - "7ca74226e2670cd08cadd5ed351fb207ff97d505cf97051abd8c35278c93a7c8"; + "425d2b1f5243dbf23c685078034b06fbfa71dc31dcce30f614e28023f140ff13"; private arePredecessorsFresh = false; private reachablePredecessors: Map = new Map(); private topoSortedIndex: Map = new Map();