Skip to content

feat(backend): the utilities and services required for the pow feature #5784

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 58 commits into from

Conversation

DecentAgeCoder
Copy link
Collaborator

@DecentAgeCoder DecentAgeCoder commented Apr 10, 2025

Motivation

This PR introduces utilities and tests to improve the worker communication framework, ensuring robust validation, seamless message handling, and secure hashing functionality. The changes enhance developer confidence in worker-related operations and establish a foundation for future scalability.

Changes

  1. Introduced routeWorkerResponse and sendMessageRequest utilities in worker.utils.ts for handling and sending worker messages with type-safe validation using Zod.
  2. Added worker.utils.spec.ts test suite to thoroughly validate the behavior of routeWorkerResponse and sendMessageRequest.
  3. Created crypto.utils.ts to provide utility functions for secure hashing (e.g., sha256, bufferToHex, hashToHex).
  4. Enhanced post-message.ts and post-message.schema.ts to centralize and type-check worker message schemas and definitions.

Tests

  1. Mocking and Setup for tests
  2. Tests for worker.utils.ts
  3. Tests for crypto.utils.ts

@DecentAgeCoder DecentAgeCoder marked this pull request as ready for review April 10, 2025 10:25
@DecentAgeCoder DecentAgeCoder requested a review from a team as a code owner April 10, 2025 10:25
@DecentAgeCoder DecentAgeCoder marked this pull request as draft April 10, 2025 10:39
DecentAgeCoder and others added 19 commits April 10, 2025 14:57
Simplifying optional param handling using toNullable()

Co-authored-by: Antonio Ventilii <antonio.ventilii@dfinity.org>
Simplifying optional param handling using toNullable()

Co-authored-by: Antonio Ventilii <antonio.ventilii@dfinity.org>
Simplifying optional param handling using toNullable()

Co-authored-by: Antonio Ventilii <antonio.ventilii@dfinity.org>
…w-worker-1' into feat/frontend/pow/introducing-pow-worker-1
…w-worker-1' into feat/frontend/pow/introducing-pow-worker-1
@DecentAgeCoder DecentAgeCoder marked this pull request as ready for review April 11, 2025 10:06
@DecentAgeCoder DecentAgeCoder enabled auto-merge (squash) April 11, 2025 11:13
*/
export const PostMessageBaseSchema = z.object({
msg: z.string(), // Message type
requestId: z.string() // Unique identifier for tracking requests and responses
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have a pre-existing base PostMessageSchema, can we expand that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not want to risk the to refactor the other messages since this structure is specifically designed for implementing the short-lived Request-Response Messaging pattern.

*/
export const PostMessageRequestBaseSchema = PostMessageBaseSchema.extend({
type: z.literal('request'), // Specifies this is a request
data: z.unknown().optional() // Optional data payload
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why unknown? don't we know what the data are?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm using a hierarchical inheritance structure for schema. The data is overridden by the actual type.

@@ -189,3 +189,92 @@ export const inferPostMessageSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
msg: z.union([PostMessageRequestSchema, PostMessageResponseSchema]),
data: dataSchema.optional()
});

// -----------------------------------------------------------------------------------------------
// The generic data structures which are required to implement the Short Polling (request-response)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, we already have quite a lot PostMessage types and schema, we use them for all the workers/schedulers. Is it not possible to use the same? I imagine you are using the same logic, no?

Comment on lines +88 to +104

// -----------------------------------------------------------------------------------------------
// The post message types used for short polling between:
// pow.worker.ts <---> worker.pow.services.ts
// -----------------------------------------------------------------------------------------------
// Base Types
export type PostMessageBase = z.infer<typeof PostMessageBaseSchema>;
export type PostMessageRequestBase = z.infer<typeof PostMessageRequestBaseSchema>;
export type PostMessageResponseBase = z.infer<typeof PostMessageResponseBaseSchema>;
export type PostMessageCreatePowChallengeRequest = z.infer<
typeof PostMessageCreatePowChallengeRequestSchema
>;
export type PostMessageCreatePowChallengeResponse = z.infer<
typeof PostMessageCreatePowChallengeResponseSchema
>;
export type PostMessageAllowSigningRequest = z.infer<typeof PostMessageAllowSigningRequestSchema>;
export type PostMessageAllowSigningResponse = z.infer<typeof PostMessageAllowSigningResponseSchema>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which one of these types is used in this PR? If they are not needed here, let's remove them (and their schemas if not needed)

Comment on lines +6 to +25
export async function sha256(input: string): Promise<ArrayBuffer> {
return await crypto.subtle.digest('SHA-256', textEncoder.encode(input));
}

/**
* Converts an ArrayBuffer to its hexadecimal string representation.
*/
export function bufferToHex(buffer: ArrayBuffer): string {
return Array.from(new Uint8Array(buffer))
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
}

/**
* Combines the hashing and hex conversion of a string into a single function.
*/
export async function hashToHex(input: string): Promise<string> {
const hashBuffer = await sha256(input);
return bufferToHex(hashBuffer);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't these functions already exists in some library that we have?

P.S. can you use arrow convention for them? for example: export const sha256 = ...


const responseHandlers = new Map<string, (data: unknown) => void>();

export function routeWorkerResponse(event: MessageEvent): boolean {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the return is a boolean? WDYT of using ResultSuccess?

import type { PostMessageRequestBase } from '$lib/types/post-message';
import type { z } from 'zod';

const responseHandlers = new Map<string, (data: unknown) => void>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this constant? why is it used? why is it not everything handled inside the worker?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of the short-lived Request-Response Messaging pattern. It is used to automatically route the request back to the sender. It is private and constant. If I move responseHandlers to pow.worker.ts I would also have to move routeWorkerResponse and sendMessageRequest to pow.worker.ts.

responseHandlers.delete(requestId);
return true;
}
console.warn('No handler found for event', requestId);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's avoid warnings

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we usually troubleshoot? Is there I dedicated debug logger that can be activated if needed? Should I remove all warnings?

data: object;
schema: z.ZodType<T>;
}): Promise<T> {
const requestId = crypto.randomUUID();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this entire function should be inside the worker. How the other workers handle this?

resolve(parsed.data);
});

worker.postMessage(payload); // Send the typed payload
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am not really sure this pertains here... usually the workers that we have handle the messaging in code. For example, have a look at class Scheduler

}
};

export const _allowSigning = async ({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this hidden method?

return nonce;
};

export const _createPowChallenge = async ({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

import { hashToHex } from '$lib/utils/crypto.utils';

function getTimestampNowMs(): bigint {
return BigInt(Date.now());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is nowInBigIntNanoSeconds maybe you can create nowInBigInt there too

difficulty: number;
}): Promise<number> => {
if (difficulty <= 0) {
throw new Error('Difficulty must be greater than zero');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i18n?

// is only used to measure the effective execution time
const startTimestampMs = getTimestampNowMs();

while (true) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's make it functional

}

let nonce = 0;
const target = Math.floor(0xffffffff / difficulty);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe some comment here

return BigInt(Date.now());
}

export const solvePowChallenge = async ({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests for this one?

suggestion: put this in another PR

}

const solveTimestampMs = getTimestampNowMs() - startTimestampMs;
console.error(`Pow Challenge solved in ${Number(solveTimestampMs) / 1e3} seconds.`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why printing an error?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was used for debug purpose. I will remove it!

auto-merge was automatically disabled April 16, 2025 07:40

Pull request was closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants