From 8d8b02298ba07cbbb0463a77d4c039a2d992f9a4 Mon Sep 17 00:00:00 2001 From: Philipp Bolte Date: Fri, 23 Aug 2024 11:02:26 +0200 Subject: [PATCH] chore: adjust error handling (#2) * chore: adjust error handling * test: add missing parameter for proof verification --- README.md | 127 +++++++++--------- src/TimestampController.ts | 30 ++--- .../TimestampController.integration.spec.ts | 20 ++- test/integration/setup/globalSetup.ts | 4 +- test/unit/TimestampController.spec.ts | 78 +++++++---- vitest-integration.config.ts | 2 + vitest-unit.config.ts | 1 + 7 files changed, 146 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index a23fb50..090a470 100644 --- a/README.md +++ b/README.md @@ -4,33 +4,34 @@ This library provides a simple interface for anchoring timestamps on the Ethereu ## Usage -To timestamp newly created data, you'll need to initialize the TimestampController by providing it with the leaves (your data) and the necessary encoding options. The library will then automatically generate a Merkle tree from your data. +To timestamp newly created data, you'll need to initialize the `TimestampController` by providing it with the leaves (your data) and the necessary encoding options. The library will then automatically generate a Merkle tree from your data. -Once you've created the timestamp, the next step is to anchor the Merkle tree's root hash to the Ethereum blockchain. This is done using the anchorRootHash method, which writes the root hash to the Ethereum blockchain's Trusted Hint Registry using the provided signer. During this process, an event is emitted that serves as proof of the data's existence at that specific point in time. +Once you've created the timestamp, the next step is to anchor the Merkle tree's root hash to the Ethereum blockchain. This is done using the `anchorRootHash` method, which writes the root hash to the Ethereum blockchain's Trusted Hint Registry using the provided signer. During this process, an event is emitted that serves as proof of the data's existence at that specific point in time. -To verify the integrity of your data later on, you can use the verifyProof method. This method requires you to provide the leaf (your data) and the corresponding Merkle proof. The verification process confirms that the data hasn't been altered since it was timestamped and anchored to the blockchain. +To verify the integrity of your data later on, you can use the `verifyProof` method. This method requires you to provide the leaf (your data) and the corresponding Merkle proof. The verification process confirms that the data hasn't been altered since it was timestamped and anchored to the blockchain. -For future verifications, you'll need to generate proofs. You can do this using either the getMerkleProof method for a single proof or the getAllMerkleProofs method if you need multiple proofs. It's highly recommended to store these proofs, along with their corresponding root hash, in a database. This practice ensures that you have easy access to the information needed for future verifications. +For future verifications, you'll need to generate proofs. You can do this using either the `getMerkleProof` method for a single proof or the `getAllMerkleProofs` method if you need multiple proofs. It's highly recommended to store these proofs, along with their corresponding root hash. This practice ensures that you have easy access to the information needed for future verifications. ```typescript import { TimestampController } from "@spherity/timestamp"; import { JsonRpcProvider, Wallet, keccak256 } from "ethers"; -// Initialize provider and signer -const provider = new JsonRpcProvider("https://infura.io/v3/YOUR-PROJECT-ID"); -const signer = new Wallet("YOUR-PRIVATE-KEY", provider); - -// Define contract options -const contractOptions = { - contractAddress: "0x1234567890123456789012345678901234567890", // Trusted Hint Registry contract address - namespace: await signer.getAddress(), // Use signer's address as namespace - list: keccak256(Buffer.from("timestampingList")); // Unique identifier for your list -}; - -// Define tree options with verifiable credentials -const treeOptions = { - leaves: [ - [ +async function main() { + // Initialize provider + const provider = new JsonRpcProvider("https://..."); + // Get signer + const signer = await provider.getSigner(); + + // Define contract options + const contractOptions = { + contractAddress: "0x1234567890123456789012345678901234567890", // Trusted Hint Registry contract address + namespace: await signer.getAddress(), // Use signer's address as namespace + list: keccak256(Buffer.from("timestampingList")), // Unique identifier for your list + }; + + // Define tree options with verifiable credentials + const treeOptions = { + leaves: [ JSON.stringify({ "@context": ["https://www.w3.org/2018/credentials/v1"], type: ["VerifiableCredential"], @@ -41,8 +42,6 @@ const treeOptions = { name: "Alice", }, }), - ], - [ JSON.stringify({ "@context": ["https://www.w3.org/2018/credentials/v1"], type: ["VerifiableCredential"], @@ -54,56 +53,55 @@ const treeOptions = { }, }), ], - ], - encoding: ["string"], -}; + encoding: "string", + }; + + // Create TimestampController instance + const controller = new TimestampController( + signer, + contractOptions, + treeOptions + ); -// Create TimestampController instance -const controller = new TimestampController(signer, contractOptions, treeOptions); + // Anchor root hash + async function anchorRootHash() { + const tx = await controller.anchorRootHash(); + console.log("Root hash anchored:", tx.hash); + } -// Anchor root hash -async function anchorRootHash() { - const tx = await controller.anchorRootHash(); - console.log("Root hash anchored:", tx.hash); -} - -// Get and verify merkle proof -async function verifyProof() { - const leaf = [ - JSON.stringify({ + // Get and verify merkle proof + async function verifyProof() { + const leaf = JSON.stringify({ "@context": ["https://www.w3.org/2018/credentials/v1"], - "type": ["VerifiableCredential"], - "issuer": "did:example:123", - "issuanceDate": "2023-06-15T00:00:00Z", - "credentialSubject": { - "id": "did:example:456", - "name": "Alice" - } - }) - ]; - const merkle = controller.getMerkleProof(leaf); - const verified = await controller.verifyProof( - leaf, - merkle.proof, - new Date("2023-06-15T00:00:00Z"), // Date when the leaf was created - 7 * 24 * 3600 // Max time difference in seconds between leaf creation and timestamp of anchoring - ); - console.log("Proof verified:", verified); -} - -// Run example -(async () => { + type: ["VerifiableCredential"], + issuer: "did:example:123", + issuanceDate: "2023-06-15T00:00:00Z", + credentialSubject: { + id: "did:example:456", + name: "Alice", + }, + }); + const merkle = controller.getMerkleProof(leaf); + const verified = await controller.verifyProof( + leaf, + merkle.proof, + new Date("2023-06-15T00:00:00Z"), // Date when the leaf was created + 7 * 24 * 3600 // Max time difference in seconds between leaf creation and timestamp of anchoring + ); + console.log("Proof verified:", verified); + } + + // Run example await anchorRootHash(); await verifyProof(); -})(); - +} ``` In case you want to verify a merkle proof later on, you can construct the TimestampController with the same contract options and an already anchored root hash. ```typescript // Create TimestampController instance with an existing root hash -const existingRootHash = "0x1234567890..."; // Replace with actual root hash +const existingRootHash = "0x1234567890..."; const controllerWithExistingRoot = new TimestampController( provider, contractOptions, @@ -124,18 +122,13 @@ async function verifyExistingProof() { }, }), ]; - const proof = ["0xabcdef...", "0x123456..."]; // Replace with actual proof + const proof = ["0xabcdef...", "0x123456..."]; const verified = await controllerWithExistingRoot.verifyProof( leaf, proof, new Date("2023-06-20T00:00:00Z"), // Date when the leaf was created - 7 * 24 * 3600 // Max time difference in seconds + 7 * 24 * 3600 // Max time difference in seconds between leaf creation and timestamp of anchoring ); console.log("Existing proof verified:", verified); } - -// Run verification with existing root hash -(async () => { - await verifyExistingProof(); -})(); ``` diff --git a/src/TimestampController.ts b/src/TimestampController.ts index c1b1737..f0114b9 100644 --- a/src/TimestampController.ts +++ b/src/TimestampController.ts @@ -148,13 +148,7 @@ class TimestampController { * @throws {TimestampControllerError} If no root hash is available or if anchoring fails. */ async anchorRootHash(): Promise { - if (!this.rootHash) { - throw new TimestampControllerError( - "No merkle tree or root hash available to anchor." - ); - } - - const key = this.rootHash; + const key = this.getRootHash(); const value = "0x1000000000000000000000000000000000000000000000000000000000000000"; @@ -183,7 +177,7 @@ class TimestampController { getMerkleProof(leaf: [any]): MerkleProof { if (!this.merkleTree) { throw new TimestampControllerError( - "No merkle tree available. Initialize with leaves to use this method." + "No merkle tree available to generate proof" ); } return { @@ -200,7 +194,7 @@ class TimestampController { getAllMerkleProofs(): MerkleProof[] { if (!this.merkleTree) { throw new TimestampControllerError( - "No merkle tree available. Initialize with leaves to use this method." + "No merkle tree available to generate proofs" ); } return Array.from(this.merkleTree.entries()).map(([index, [value]]) => { @@ -228,14 +222,10 @@ class TimestampController { leaf: [any], proof: string[], leafCreationTime: Date, - maxTimeDifference: number = 30 * 24 * 3600, + maxTimeDifference: number, leafEncoding: string[] = ["string"] ): Promise<{ verified: boolean; reason?: string }> { - if (!this.rootHash) { - throw new TimestampControllerError( - "No root hash available. Initialize with leaves or provide a root hash." - ); - } + const rootHash = this.getRootHash(); const events = await this.getHintValueChangedEvents(); if (events.length === 0) { @@ -268,7 +258,7 @@ class TimestampController { } const verified = StandardMerkleTree.verify( - this.rootHash, + rootHash, leafEncoding, leaf, proof @@ -297,10 +287,16 @@ class TimestampController { * @param event - The event to get the timestamp for. * @returns A promise that resolves to the block timestamp. * @private + * @throws {TimestampControllerError} If no timestamp can be retrieved */ private async getRootHashBlockTimestamp(event: EventLog): Promise { const block = await this.provider.getBlock(event.blockNumber); - return block!.timestamp; + if (!block?.timestamp) { + throw new TimestampControllerError( + `Failed to get block timestamp for event transaction ${event.transactionHash}` + ); + } + return block.timestamp; } /** diff --git a/test/integration/TimestampController.integration.spec.ts b/test/integration/TimestampController.integration.spec.ts index 386c00a..e1a67be 100644 --- a/test/integration/TimestampController.integration.spec.ts +++ b/test/integration/TimestampController.integration.spec.ts @@ -60,10 +60,13 @@ describe("TimestampController (Integration)", () => { .then((block) => block!.timestamp); await controller.anchorRootHash(); const proof = controller.getMerkleProof(["data1"]); + const leafCreationTime = new Date(currentBlockTime); + const maxTimeDifference = 30 * 24 * 3600; const verified = await controller.verifyProof( proof.leaf, proof.proof, - new Date(currentBlockTime) + leafCreationTime, + maxTimeDifference ); expect(proof).toBeDefined(); @@ -76,12 +79,15 @@ describe("TimestampController (Integration)", () => { .then((block) => block!.timestamp); await controller.anchorRootHash(); const proofs = controller.getAllMerkleProofs(); + const leafCreationTime = new Date(currentBlockTime); + const maxTimeDifference = 30 * 24 * 3600; const verified = await Promise.all( proofs.map((proof) => controller.verifyProof( proof.leaf, proof.proof, - new Date(currentBlockTime) + leafCreationTime, + maxTimeDifference ) ) ); @@ -94,10 +100,13 @@ describe("TimestampController (Integration)", () => { it("should fail to verify wrong proof", async () => { await controller.anchorRootHash(); const proof = controller.getMerkleProof(["data1"]); + const leafCreationTime = new Date(); + const maxTimeDifference = 30 * 24 * 3600; const verified = await controller.verifyProof( ["data2"], proof.proof, - new Date() + leafCreationTime, + maxTimeDifference ); expect(proof).toBeDefined(); @@ -106,10 +115,13 @@ describe("TimestampController (Integration)", () => { it("should fail to verify proof for non-anchored root hash", async () => { const proof = controller.getMerkleProof(["data1"]); + const leafCreationTime = new Date(); + const maxTimeDifference = 30 * 24 * 3600; const verified = await controller.verifyProof( proof.leaf, proof.proof, - new Date() + leafCreationTime, + maxTimeDifference ); expect(proof).toBeDefined(); diff --git a/test/integration/setup/globalSetup.ts b/test/integration/setup/globalSetup.ts index 2c8a6e4..a70238d 100644 --- a/test/integration/setup/globalSetup.ts +++ b/test/integration/setup/globalSetup.ts @@ -3,8 +3,8 @@ import { FORK_BLOCK_NUMBER, FORK_URL } from "./utils"; export default async function () { return await startProxy({ - port: 8545, // By default, the proxy will listen on port 8545. - host: "::", // By default, the proxy will listen on all interfaces. + port: 8545, + host: "::", options: { chainId: 11155111, forkUrl: FORK_URL, diff --git a/test/unit/TimestampController.spec.ts b/test/unit/TimestampController.spec.ts index c8cf06f..17917e0 100644 --- a/test/unit/TimestampController.spec.ts +++ b/test/unit/TimestampController.spec.ts @@ -266,11 +266,14 @@ describe("TimestampController", () => { "0x1234567890123456789012345678901234567890123456789012345678901234", } ); + const leafCreationTime = new Date(CURRENT_BLOCK_TIMESTAMP); + const maxTimeDifference = 30 * 24 * 3600; const result = await controller.verifyProof( ["leaf1"], ["proof1", "proof2"], - new Date(CURRENT_BLOCK_TIMESTAMP) + leafCreationTime, + maxTimeDifference ); expect(result).toEqual({ verified: true }); @@ -301,7 +304,8 @@ describe("TimestampController", () => { const result = await controller.verifyProof( ["leaf1"], ["proof1", "proof2"], - new Date() + new Date(), + 30 * 24 * 3600 ); expect(result).toEqual({ @@ -380,7 +384,7 @@ describe("TimestampController", () => { ); await expect(controller.anchorRootHash()).rejects.toThrow( - "No merkle tree or root hash available to anchor." + "No root hash available. Initialize with leaves or provide a root hash." ); }); @@ -471,7 +475,12 @@ describe("TimestampController", () => { }); await expect( - controller.verifyProof(["leaf1"], ["proof1", "proof2"], new Date()) + controller.verifyProof( + ["leaf1"], + ["proof1", "proof2"], + new Date(), + 30 * 24 * 3600 + ) ).rejects.toThrow(TimestampControllerError); }); @@ -529,11 +538,14 @@ describe("TimestampController", () => { "0x1234567890123456789012345678901234567890123456789012345678901234", } ); + const leafCreationTime = new Date(CURRENT_BLOCK_TIMESTAMP); + const maxTimeDifference = 30 * 24 * 3600; const result = await controller.verifyProof( ["leaf1"], ["proof1", "proof2"], - new Date(CURRENT_BLOCK_TIMESTAMP) + leafCreationTime, + maxTimeDifference ); expect(result).toEqual({ @@ -542,7 +554,7 @@ describe("TimestampController", () => { }); }); - it("should use default maxTimeDifference when not provided", async () => { + it("should use custom leafEncoding when provided", async () => { vi.mocked(StandardMerkleTree.verify).mockReturnValue(true); vi.mocked(mockContract.queryFilter).mockResolvedValue([ { @@ -552,9 +564,6 @@ describe("TimestampController", () => { }, }, ] as unknown as EventLog[]); - vi.mocked(mockProvider.getBlock).mockResolvedValue({ - timestamp: Date.now(), - } as any); const controller = new TimestampController( mockProvider, @@ -569,26 +578,38 @@ describe("TimestampController", () => { } ); - const result = await controller.verifyProof( + const verified = await controller.verifyProof( ["leaf1"], ["proof1", "proof2"], - new Date() + new Date(CURRENT_BLOCK_TIMESTAMP), + 30 * 24 * 3600, + ["bytes32"] ); - expect(result).toEqual({ verified: true }); + expect(StandardMerkleTree.verify).toHaveBeenCalledWith( + "0x1234567890123456789012345678901234567890123456789012345678901234", + ["bytes32"], + ["leaf1"], + ["proof1", "proof2"] + ); }); - it("should use custom leafEncoding when provided", async () => { - vi.mocked(StandardMerkleTree.verify).mockReturnValue(true); + it("should throw an error when block timestamp is not available", async () => { vi.mocked(mockContract.queryFilter).mockResolvedValue([ { args: { value: "0x1000000000000000000000000000000000000000000000000000000000000000", }, + blockNumber: 12345, + transactionHash: "0xabcdef1234567890", }, ] as unknown as EventLog[]); + vi.mocked(mockProvider.getBlock).mockResolvedValue({ + timestamp: undefined, + } as any); + const controller = new TimestampController( mockProvider, { @@ -602,19 +623,24 @@ describe("TimestampController", () => { } ); - const verified = await controller.verifyProof( - ["leaf1"], - ["proof1", "proof2"], - new Date(CURRENT_BLOCK_TIMESTAMP), - 30 * 24 * 3600, - ["bytes32"] - ); + await expect( + controller.verifyProof( + ["leaf1"], + ["proof1", "proof2"], + new Date(), + 30 * 24 * 3600 + ) + ).rejects.toThrow(TimestampControllerError); - expect(StandardMerkleTree.verify).toHaveBeenCalledWith( - "0x1234567890123456789012345678901234567890123456789012345678901234", - ["bytes32"], - ["leaf1"], - ["proof1", "proof2"] + await expect( + controller.verifyProof( + ["leaf1"], + ["proof1", "proof2"], + new Date(), + 30 * 24 * 3600 + ) + ).rejects.toThrow( + "Failed to get block timestamp for event transaction 0xabcdef1234567890" ); }); }); diff --git a/vitest-integration.config.ts b/vitest-integration.config.ts index 02edacd..c19e1d6 100644 --- a/vitest-integration.config.ts +++ b/vitest-integration.config.ts @@ -1,6 +1,8 @@ import { defineConfig } from "vitest/config"; + export default defineConfig({ test: { + globals: true, globalSetup: ["./test/integration/setup/globalSetup.ts"], setupFiles: ["./test/integration/setup/setup.ts"], include: ["./test/integration/*.integration.spec.ts"], diff --git a/vitest-unit.config.ts b/vitest-unit.config.ts index 8df1f38..6ec00d2 100644 --- a/vitest-unit.config.ts +++ b/vitest-unit.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from "vitest/config"; + export default defineConfig({ test: { globals: true,