diff --git a/package.json b/package.json index 6d0ec18..df50cb3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@spherity/timestamp", - "version": "1.0.0", + "version": "1.2.0", "description": "A library to create timestamps with merkle trees and the ERC-7506 trusted hint registry", "type": "module", "main": "./dist/index.cjs", diff --git a/src/TimestampController.ts b/src/TimestampController.ts index f0114b9..0371790 100644 --- a/src/TimestampController.ts +++ b/src/TimestampController.ts @@ -56,6 +56,7 @@ class TimestampController { private readonly contract: TypedContract; private merkleTree?: StandardMerkleTree; private rootHash?: string; + private encoding?: string[]; private readonly contractOptions: ContractOptions; /** @@ -117,6 +118,7 @@ class TimestampController { */ private createMerkleTree(options: TreeOptions): void { try { + this.encoding = options.encoding; this.merkleTree = StandardMerkleTree.of(options.leaves, options.encoding); this.rootHash = this.merkleTree.root; } catch (error) { @@ -316,6 +318,44 @@ class TimestampController { const timeDifference = Math.abs(rootHashTimestamp - leafCreationTimestamp); return timeDifference <= maxTimeDifference; } + + /** + * Adds new leaves to the existing Merkle tree. + * @param newLeaves - An array of new leaves to add to the tree. + * @throws {TimestampControllerError} If no Merkle tree is available or if adding leaves fails. + */ + addLeaves(newLeaves: any[]): void { + if (!this.merkleTree) { + throw new TimestampControllerError( + "No merkle tree available. Initialize with leaves first." + ); + } + if (!this.encoding) { + throw new TimestampControllerError( + "No encoding was provided to extend the merkle tree." + ); + } + + try { + const currentLeaves = Array.from(this.merkleTree.entries()).map( + ([, value]) => value + ); + + const updatedLeaves = [...currentLeaves, ...newLeaves]; + + // Recreate the Merkle tree with updated leaves + this.createMerkleTree({ + leaves: updatedLeaves, + encoding: this.encoding, + }); + } catch (error) { + throw new TimestampControllerError( + `Failed to add leaves to Merkle tree: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } } export { TimestampController, TimestampControllerError, VerificationFailure }; diff --git a/test/integration/TimestampController.integration.spec.ts b/test/integration/TimestampController.integration.spec.ts index e1a67be..f5e0c7e 100644 --- a/test/integration/TimestampController.integration.spec.ts +++ b/test/integration/TimestampController.integration.spec.ts @@ -131,4 +131,51 @@ describe("TimestampController (Integration)", () => { it("should fail to get proof for non-existent leaf", () => { expect(() => controller.getMerkleProof(["non-existent-data"])).toThrow(); }); + + it("should successfully add new leaves, anchor the new root hash, and verify proofs", async () => { + // Add new leaves + const newLeaves = [["data4"], ["data5"]]; + controller.addLeaves(newLeaves); + + // Get the new root hash after adding leaves + const newRootHash = controller.getRootHash(); + + // Anchor the new root hash + const tx = await controller.anchorRootHash(); + await tx.wait(); // Wait for the transaction to be mined + + // Verify that the correct root hash was anchored + const anchoredHintValue = await hintRegistry.getHint( + namespace, + list, + newRootHash + ); + + expect(anchoredHintValue).toBe( + "0x1000000000000000000000000000000000000000000000000000000000000000" + ); + + // Get the current block time for leaf creation time + const currentBlockTime = await provider + .getBlock("latest") + .then((block) => block!.timestamp); + const leafCreationTime = new Date(currentBlockTime); // Convert to milliseconds + const maxTimeDifference = 30 * 24 * 3600; // 30 days in seconds + + const allProofs = controller.getAllMerkleProofs(); + expect(allProofs.length).toBe(leaves.length + newLeaves.length); + + const allVerified = await Promise.all( + allProofs.map((proof) => + controller.verifyProof( + proof.leaf, + proof.proof, + leafCreationTime, + maxTimeDifference + ) + ) + ); + + expect(allVerified.every((v) => v.verified)).toBe(true); + }); }); diff --git a/test/unit/TimestampController.spec.ts b/test/unit/TimestampController.spec.ts index 17917e0..a4b73f1 100644 --- a/test/unit/TimestampController.spec.ts +++ b/test/unit/TimestampController.spec.ts @@ -643,4 +643,108 @@ describe("TimestampController", () => { "Failed to get block timestamp for event transaction 0xabcdef1234567890" ); }); + + it("should add new leaves to the existing Merkle tree", () => { + const initialLeaves = [["data1"], ["data2"]]; + const newLeaves = [["data3"], ["data4"]]; + const allLeaves = [...initialLeaves, ...newLeaves]; + + vi.mocked(StandardMerkleTree.of).mockReturnValueOnce({ + root: "0x1234567890123456789012345678901234567890123456789012345678901234", + getProof: vi.fn().mockReturnValue(["proof1", "proof2"]), + entries: vi.fn().mockReturnValue([ + [0, ["data1"]], + [1, ["data2"]], + ]), + } as unknown as StandardMerkleTree); + + const controller = new TimestampController( + mockProvider, + { + contractAddress: "0x0000000000000000000000000000000000000000", + namespace: "testNamespace", + list: "testList", + }, + { leaves: initialLeaves, encoding: ["string"] } + ); + + expect(StandardMerkleTree.of).toHaveBeenCalledWith(initialLeaves, [ + "string", + ]); + expect(controller.getRootHash()).toBe( + "0x1234567890123456789012345678901234567890123456789012345678901234" + ); + + vi.mocked(StandardMerkleTree.of).mockClear(); + vi.mocked(StandardMerkleTree.of).mockReturnValueOnce({ + root: "0x9876543210987654321098765432109876543210987654321098765432109876", + getProof: vi + .fn() + .mockReturnValue(["proof1", "proof2", "proof3", "proof4"]), + entries: vi.fn().mockReturnValue([ + [0, ["data1"]], + [1, ["data2"]], + [2, ["data3"]], + [3, ["data4"]], + ]), + } as unknown as StandardMerkleTree); + + controller.addLeaves(newLeaves); + + expect(StandardMerkleTree.of).toHaveBeenCalledWith(allLeaves, ["string"]); + + expect(controller.getRootHash()).toBe( + "0x9876543210987654321098765432109876543210987654321098765432109876" + ); + }); + + it("should throw an error when trying to add leaves without an initialized Merkle tree", () => { + const controller = new TimestampController( + mockProvider, + { + contractAddress: "0x0000000000000000000000000000000000000000", + namespace: "testNamespace", + list: "testList", + }, + { rootHash: "0x1234" } + ); + + expect(() => controller.addLeaves([["newData"]])).toThrow( + "No merkle tree available. Initialize with leaves first." + ); + }); + + it("should maintain the correct encoding when adding new leaves", () => { + const initialLeaves = [ + ["0x1111", "5000000000000000000"], + ["0x2222", "2500000000000000000"], + ]; + const newLeaves = [["0x3333", "1000000000000000000"]]; + const allLeaves = [...initialLeaves, ...newLeaves]; + + vi.mocked(StandardMerkleTree.of).mockReturnValue({ + root: "0x1234", + getProof: vi.fn(), + entries: vi.fn().mockReturnValue(initialLeaves.entries()), + } as unknown as StandardMerkleTree); + + const controller = new TimestampController( + mockProvider, + { + contractAddress: "0x0000000000000000000000000000000000000000", + namespace: "testNamespace", + list: "testList", + }, + { leaves: initialLeaves, encoding: ["address", "uint256"] } + ); + + vi.mocked(StandardMerkleTree.of).mockClear(); + + controller.addLeaves(newLeaves); + + expect(StandardMerkleTree.of).toHaveBeenCalledWith(allLeaves, [ + "address", + "uint256", + ]); + }); });