Forráskód Böngészése

Versions are identified by the XOR of the hashes of the deltas they consist of

Joeri Exelmans 3 éve
szülő
commit
b2318ae4c9

+ 11 - 0
src/onion/buffer_xor.ts

@@ -0,0 +1,11 @@
+
+// Precondition that is NOT CHECKED: buffers must be of equal length
+// Returns new buffer that is bitwise XOR of inputs.
+export function bufferXOR(a: Buffer, b: Buffer): Buffer {
+  const result = Buffer.allocUnsafe(a.length);
+  for (let i=0; i<a.length; i+=4) {
+    // Little endian is fastest, because native to Intel CPUs
+    result.writeInt32LE(a.readInt32LE(i) ^ b.readInt32LE(i), i);
+  }
+  return result;
+}

+ 0 - 1
src/onion/composite_delta.test.ts

@@ -7,7 +7,6 @@ import {
 
 import {
   CompositeLevel,
-  CompositeDelta,
 } from "./composite_delta";
 
 import {

+ 15 - 4
src/onion/composite_delta.ts

@@ -1,12 +1,15 @@
+import {createHash} from "crypto";
 import {Delta} from "./delta";
 
-export class CompositeDelta implements Delta {
+class CompositeDelta implements Delta {
   readonly dependencies: Array<CompositeDelta>;
   readonly conflicts: Array<CompositeDelta>;
+  readonly hash: Buffer;
 
-  constructor(dependencies: Array<CompositeDelta>, conflicts: Array<CompositeDelta>) {
+  constructor(dependencies: Array<CompositeDelta>, conflicts: Array<CompositeDelta>, hash: Buffer) {
     this.dependencies = dependencies;
     this.conflicts = conflicts;
+    this.hash = hash;
   }
 
   getDependencies(): Array<CompositeDelta> {
@@ -16,12 +19,16 @@ export class CompositeDelta implements Delta {
   getConflicts(): Array<CompositeDelta> {
     return this.conflicts;
   }
+
+  getHash(): Buffer {
+    return this.hash;
+  }
 }
 
 export class CompositeLevel {
   containedBy: Map<Delta, CompositeDelta> = new Map();
 
-  createComposite(deltas: Array<Delta>) {
+  createComposite(deltas: Array<Delta>): CompositeDelta {
     const dependencies: Array<CompositeDelta> = [];
     const conflicts: Array<CompositeDelta> = [];
 
@@ -53,7 +60,11 @@ export class CompositeLevel {
       throw new Error("Assertion failed: overlap between dependencies and conflicts");
     }
 
-    const composite = new CompositeDelta(dependencies, conflicts);
+    const hash = createHash('sha256');
+    for (const delta of deltas) {
+      hash.update(delta.getHash());
+    }
+    const composite = new CompositeDelta(dependencies, conflicts, hash.digest());
 
     for (const delta of deltas) {
       this.containedBy.set(delta, composite);

+ 7 - 0
src/onion/delta.ts

@@ -1,6 +1,13 @@
 export interface Delta {
+
   // Get deltas that this delta depends on.
   getDependencies(): Array<Delta>;
+
   // Get deltas that are conflicting with this delta.
   getConflicts(): Array<Delta>;
+
+  // Get an ID that is unique to the VALUE of this Delta.
+  // Meaning: if two deltas are identical, they have the same ID.
+  // Returned value must be 8 bytes (256 bits).
+  getHash(): Buffer;
 }

+ 3 - 0
src/onion/micro_op.test.ts

@@ -29,6 +29,9 @@ describe("Delta", () => {
     const deletion2 = new NodeDeletion(creation, []);
     assert(deletion1.getConflicts().length === 1, "expected second deletion to be conflicting with first deletion");
     assert(deletion2.getConflicts().length === 1, "expected second deletion to be conflicting with first deletion");
+
+
+    assert(deletion1.getHash().equals(deletion2.getHash()), "deletions should have equal hash");
   });
 
   it("Create/create edge conflict", () => {

+ 43 - 0
src/onion/micro_op.ts

@@ -1,11 +1,14 @@
 import {UUID} from "./types";
 
+import {createHash} from "crypto";
+
 import {
   Delta,
 } from "./delta";
 
 export class NodeCreation implements Delta {
   readonly id: UUID;
+  readonly hash: Buffer;
 
   // Inverse dependency: Deletions of this node.
   deletions: Array<NodeDeletion> = []; // append-only
@@ -18,6 +21,10 @@ export class NodeCreation implements Delta {
 
   constructor(id: UUID) {
     this.id = id;
+    this.hash = createHash('sha256')
+      .update(typeof id) // prevent collisions between 'true' (actual boolean) and '"true"' (string "true")
+      .update(this.id.value.toString())
+      .digest();
   }
 
   getDependencies(): [] {
@@ -27,9 +34,15 @@ export class NodeCreation implements Delta {
   getConflicts(): [] {
     return [];
   }
+
+  getHash(): Buffer {
+    return this.hash;
+  }
 }
 
 export class NodeDeletion implements Delta {
+  readonly hash: Buffer;
+
   // Dependency: The node being deleted.
   readonly creation: NodeCreation;
 
@@ -52,6 +65,9 @@ export class NodeDeletion implements Delta {
     this.creation = creation;
     this.deletedEdges = deletedEdges;
 
+    // id is the hash of the creation's id (which is the hash of the UUID of the node :)
+    this.hash = createHash('sha256').update(this.creation.hash).digest();
+
     // Detect conflicts
 
     // Delete/delete
@@ -116,6 +132,10 @@ export class NodeDeletion implements Delta {
       this.updateConflicts,
     );
   }
+
+  getHash(): Buffer {
+    return this.hash;
+  }
 }
 
 // Common functionality in EdgeCreation and EdgeUpdate: both set the target of an edge, and this can conflict with the deletion of the target.
@@ -159,6 +179,8 @@ export class EdgeCreation implements Delta {
   readonly label: string;
   readonly target: SetsTarget;
 
+  readonly hash: Buffer;
+
   // Inverse dependency
   // NodeDeletion if source of edge is deleted.
   overwrittenBy: Array<EdgeUpdate | NodeDeletion> = []; // append-only
@@ -174,6 +196,12 @@ export class EdgeCreation implements Delta {
     this.label = label;
     this.target = new SetsTarget(this, target);
 
+    this.hash = createHash('sha256')
+      .update(source.hash)
+      .update('create').update(label)
+      .update('target').update(target.hash)
+      .digest();
+
     // Detect conflicts
 
     // Create/create
@@ -214,6 +242,10 @@ export class EdgeCreation implements Delta {
       this.target.deleteTargetConflicts,
     );
   }
+
+  getHash(): Buffer {
+    return this.hash;
+  }
 }
 
 export class EdgeUpdate implements Delta {
@@ -221,6 +253,8 @@ export class EdgeUpdate implements Delta {
   readonly overwrites: EdgeCreation | EdgeUpdate;
   readonly target: SetsTarget;
 
+  readonly hash: Buffer;
+
   // Inverse dependency
   // NodeDeletion if source of edge is deleted.
   overwrittenBy: Array<EdgeUpdate | NodeDeletion> = []; // append-only
@@ -232,6 +266,11 @@ export class EdgeUpdate implements Delta {
     this.overwrites = overwrites;
     this.target = new SetsTarget(this, newTarget);
 
+    this.hash = createHash('sha256')
+      .update(overwrites.hash)
+      .update('target').update(newTarget.hash)
+      .digest();
+
     // Detect conflicts
 
     // Concurrent updates (by EdgeUpdate or NodeDeletion)
@@ -260,5 +299,9 @@ export class EdgeUpdate implements Delta {
       this.target.deleteTargetConflicts,
     );
   }
+
+  getHash(): Buffer {
+    return this.hash;
+  }
 }
 

+ 18 - 13
src/onion/version.test.ts

@@ -1,7 +1,7 @@
 import * as _ from "lodash";
 
 import {
-  Version,
+  VersionRegistry,
 } from "./version";
 
 import {
@@ -18,27 +18,32 @@ describe("Version", () => {
 
   it("Get deltas", () => {
     const getId = getUuidCallback();
+    const registry = new VersionRegistry();
 
     const nodeCreation = new NodeCreation(getId());
     const nodeDeletion = new NodeDeletion(nodeCreation, []);
 
-    const version0 = new Version([]);
-    const version1 = new Version([[version0, nodeCreation]]);
-    const version2 = new Version([[version1, nodeDeletion]]);
+    const version1 = registry.createVersion(registry.initialVersion, nodeCreation);
+    const version2 = registry.createVersion(version1, nodeDeletion);
 
-    assert(_.isEqual(version0.getDeltas(), []), "expected version0 to be empty");
+    assert(_.isEqual(registry.initialVersion.getDeltas(), []), "expected initialVersion to be empty");
     assert(_.isEqual(version1.getDeltas(), [nodeCreation]), "expected version0 to be empty");
     assert(_.isEqual(version2.getDeltas(), [nodeCreation, nodeDeletion]), "expected version0 to be empty");
-
   });
 
-  // it("Invalid version", () => {
-  //   const getId = getUuidCallback();
+  it("Commutating operations yield equal versions", () => {
+    const getId = getUuidCallback();
+    const registry = new VersionRegistry();
+
+    const nodeCreationA = new NodeCreation(getId());
+    const nodeCreationB = new NodeCreation(getId());
 
-  //   const nodeCreation = new NodeCreation(getId());
-  //   const nodeDeletion = new NodeDeletion(nodeCreation, []);
+    const versionA = registry.createVersion(registry.initialVersion, nodeCreationA);
+    const versionAB = registry.createVersion(versionA, nodeCreationB);
 
-  //   const version0 = new Version([]);
-  //   const version1 = new Version([[version0, nodeDeletion]]); // missing dependency: nodeCreation
-  // })
+    const versionB = registry.createVersion(registry.initialVersion, nodeCreationB);
+    const versionBA = registry.createVersion(versionB, nodeCreationA);
+
+    assert(versionAB === versionBA, "expected versions to be equal");
+  });
 });

+ 64 - 14
src/onion/version.ts

@@ -2,26 +2,76 @@ import {
   Delta,
 } from "./delta";
 
+import {bufferXOR} from "./buffer_xor";
 
-export class Version {
+class Version {
   readonly parents: Array<[Version, Delta]>;
 
-  constructor(parents: Array<[Version, Delta]>) {
+  // XOR of all delta hashes
+  readonly hash: Buffer;
+
+  constructor(parents: Array<[Version, Delta]>, hash: Buffer) {
     this.parents = parents;
+    this.hash = hash;
+
+    // if (parents.length > 0) {
+    //   const [parentVersion, delta] = parents[0];
+    //   this.hash = bufferXOR(parentVersion.hash, delta.getHash());
+
+    //   for (const [parentVersion, delta] of parents.slice(1)) {
+    //     if (! this.hash.equals(bufferXOR(parentVersion.hash, delta.getHash()))) {
+    //       throw new Error("Assertion failed: all paths to this version must yield same hash.");
+    //     }
+    //   }
+    // } else {
+    //   // Empty version has ID with all zeros
+    //   this.hash = Buffer.alloc(32);
+    // }
   }
 
-  // slow!
+  // Depth-first recursion and lots of array concatentations -> slow!
   getDeltas(): Array<Delta> {
-    return Array<Delta>().concat(... // flatten the following array of arrays (type: Delta[][])
-      this.parents.map(([parentVersion, delta]) => [...parentVersion.getDeltas(), delta])
-    );
+    if (this.parents.length === 0) {
+      return [];
+    } else {
+      const [parentVersion, delta] = this.parents[0];
+      return parentVersion.getDeltas().concat([delta]);
+    }
+
+    // return Array<Delta>().concat(... // flatten the following array of arrays (type: Delta[][])
+    //   this.parents.map(([parentVersion, delta]) => [...parentVersion.getDeltas(), delta])
+    // );
+  }
+}
+
+export class VersionRegistry {
+  readonly versionMap: Map<string, Version> = new Map();
+  readonly initialVersion: Version;
+
+  constructor() {
+    const initialHash = Buffer.alloc(32); // all zeros
+    this.initialVersion = new Version([], initialHash);
+    this.versionMap.set(initialHash.toString('base64'), this.initialVersion);
   }
 
-  // findDelta(d: Delta): Version {
-  //   for (const [parentVersion, delta] of this.parents) {
-  //     if (delta === d) {
-  //       return this;
-  //     }
-  //   }
-  // }
-}
+  createVersion(parent: Version, delta: Delta): Version {
+    const hash = bufferXOR(parent.hash, delta.getHash());
+    const existingVersion = this.versionMap.get(hash.toString('base64'));
+    if (existingVersion !== undefined) {
+      const havePath = existingVersion.parents.some(([parentVersion, delta]) => parentVersion === parent);
+      if (!havePath) {
+        existingVersion.parents.push([parent, delta]);
+      }
+      return existingVersion;
+    } else {
+      const version = new Version([[parent, delta]], hash);
+      this.versionMap.set(hash.toString('base64'), version);
+      return version;
+    }
+  }
+}
+
+
+// export function mergeVersions(versionA: Version, versionB: Version): Array<Version> {
+//   versionA
+// }